2018-11-03 23:25:08 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
2021-10-08 16:23:45 +00:00
|
|
|
use App\Exceptions\NonSmartPlaylistException;
|
|
|
|
use App\Factories\SmartPlaylistRuleParameterFactory;
|
2018-11-03 23:25:08 +00:00
|
|
|
use App\Models\Playlist;
|
|
|
|
use App\Models\Song;
|
|
|
|
use App\Models\User;
|
2021-10-08 16:23:45 +00:00
|
|
|
use App\Values\SmartPlaylistRule;
|
|
|
|
use App\Values\SmartPlaylistRuleGroup;
|
2018-11-03 23:25:08 +00:00
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
2021-10-08 16:23:45 +00:00
|
|
|
use Illuminate\Support\Collection;
|
2018-11-03 23:25:08 +00:00
|
|
|
|
|
|
|
class SmartPlaylistService
|
|
|
|
{
|
2021-10-08 16:23:45 +00:00
|
|
|
private const USER_REQUIRING_RULE_PREFIXES = ['interactions.'];
|
|
|
|
|
|
|
|
private SmartPlaylistRuleParameterFactory $parameterFactory;
|
|
|
|
|
|
|
|
public function __construct(SmartPlaylistRuleParameterFactory $parameterFactory)
|
|
|
|
{
|
|
|
|
$this->parameterFactory = $parameterFactory;
|
|
|
|
}
|
2018-11-03 23:25:08 +00:00
|
|
|
|
2020-12-22 20:11:22 +00:00
|
|
|
/** @return Collection|array<Song> */
|
2018-11-03 23:25:08 +00:00
|
|
|
public function getSongs(Playlist $playlist): Collection
|
|
|
|
{
|
2021-10-08 16:23:45 +00:00
|
|
|
throw_unless($playlist->is_smart, NonSmartPlaylistException::create($playlist));
|
2018-11-03 23:25:08 +00:00
|
|
|
|
2021-10-08 16:23:45 +00:00
|
|
|
$ruleGroups = $this->addRequiresUserRules($playlist->rule_groups, $playlist->user);
|
2018-11-03 23:25:08 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
return $this->buildQueryFromRules($ruleGroups, $playlist->user)
|
|
|
|
->orderBy('songs.title')
|
|
|
|
->get();
|
2018-11-03 23:25:08 +00:00
|
|
|
}
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
public function buildQueryFromRules(Collection $ruleGroups, User $user): Builder
|
2018-11-03 23:25:08 +00:00
|
|
|
{
|
2022-06-10 10:47:46 +00:00
|
|
|
$query = Song::withMeta($user);
|
2018-11-03 23:25:08 +00:00
|
|
|
|
2021-10-08 16:23:45 +00:00
|
|
|
$ruleGroups->each(function (SmartPlaylistRuleGroup $group) use ($query): void {
|
|
|
|
$query->orWhere(function (Builder $subQuery) use ($group): void {
|
|
|
|
$group->rules->each(function (SmartPlaylistRule $rule) use ($subQuery): void {
|
|
|
|
$this->buildQueryForRule($subQuery, $rule);
|
|
|
|
});
|
2018-11-18 21:50:03 +00:00
|
|
|
});
|
2018-11-03 23:25:08 +00:00
|
|
|
});
|
2018-11-18 21:50:03 +00:00
|
|
|
|
|
|
|
return $query;
|
2018-11-03 23:25:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Some rules need to be driven by an additional "user" factor, for example play count, liked, or last played
|
|
|
|
* (basically everything related to interactions).
|
|
|
|
* For those, we create an additional "user_id" rule.
|
|
|
|
*
|
2021-10-08 16:23:45 +00:00
|
|
|
* @return Collection|array<SmartPlaylistRuleGroup>
|
2018-11-03 23:25:08 +00:00
|
|
|
*/
|
2021-10-08 16:23:45 +00:00
|
|
|
public function addRequiresUserRules(Collection $ruleGroups, User $user): Collection
|
2018-11-03 23:25:08 +00:00
|
|
|
{
|
2021-10-08 16:23:45 +00:00
|
|
|
return $ruleGroups->map(function (SmartPlaylistRuleGroup $group) use ($user): SmartPlaylistRuleGroup {
|
|
|
|
$clonedGroup = clone $group;
|
|
|
|
$additionalRules = collect();
|
2018-11-03 23:25:08 +00:00
|
|
|
|
2021-10-08 16:23:45 +00:00
|
|
|
$group->rules->each(function (SmartPlaylistRule $rule) use ($additionalRules, $user): void {
|
|
|
|
foreach (self::USER_REQUIRING_RULE_PREFIXES as $modelPrefix) {
|
|
|
|
if (starts_with($rule->model, $modelPrefix)) {
|
|
|
|
$additionalRules->add($this->createRequiresUserRule($user, $modelPrefix));
|
2018-11-18 21:50:03 +00:00
|
|
|
}
|
2018-11-03 23:25:08 +00:00
|
|
|
}
|
2021-10-08 16:23:45 +00:00
|
|
|
});
|
2018-11-18 21:50:03 +00:00
|
|
|
|
2021-10-08 16:23:45 +00:00
|
|
|
// Make sure all those additional rules are unique.
|
|
|
|
$clonedGroup->rules = $clonedGroup->rules->merge($additionalRules->unique('model')->collect());
|
2018-11-03 23:25:08 +00:00
|
|
|
|
2021-10-08 16:23:45 +00:00
|
|
|
return $clonedGroup;
|
|
|
|
});
|
2018-11-03 23:25:08 +00:00
|
|
|
}
|
|
|
|
|
2021-10-08 16:23:45 +00:00
|
|
|
private function createRequiresUserRule(User $user, string $modelPrefix): SmartPlaylistRule
|
2018-11-03 23:25:08 +00:00
|
|
|
{
|
2021-10-08 16:23:45 +00:00
|
|
|
return SmartPlaylistRule::create([
|
2020-12-22 20:11:22 +00:00
|
|
|
'model' => $modelPrefix . 'user_id',
|
2018-11-03 23:25:08 +00:00
|
|
|
'operator' => 'is',
|
|
|
|
'value' => [$user->id],
|
2021-10-08 16:23:45 +00:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function buildQueryForRule(Builder $query, SmartPlaylistRule $rule, ?string $model = null): Builder
|
|
|
|
{
|
|
|
|
if (!$model) {
|
|
|
|
$model = $rule->model;
|
|
|
|
}
|
|
|
|
|
|
|
|
$fragments = explode('.', $model, 2);
|
|
|
|
|
|
|
|
if (count($fragments) === 1) {
|
|
|
|
return $query->{$this->resolveWhereLogic($rule)}(
|
|
|
|
...$this->parameterFactory->createParameters($model, $rule->operator, $rule->value)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the model is something like 'artist.name' or 'interactions.play_count', we have a subquery to deal with.
|
|
|
|
// We handle such a case with a recursive call which, in theory, should work with an unlimited level of nesting,
|
|
|
|
// though in practice we only have one level max.
|
|
|
|
return $query->whereHas(
|
|
|
|
$fragments[0],
|
|
|
|
fn (Builder $subQuery) => $this->buildQueryForRule($subQuery, $rule, $fragments[1])
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolve the logic of a (sub)query base on the configured operator.
|
|
|
|
* Basically, if the operator is "between," we use "whereBetween". Otherwise, it's "where". Simple.
|
|
|
|
*/
|
|
|
|
private function resolveWhereLogic(SmartPlaylistRule $rule): string
|
|
|
|
{
|
|
|
|
return $rule->operator === SmartPlaylistRule::OPERATOR_IS_BETWEEN ? 'whereBetween' : 'where';
|
2018-11-03 23:25:08 +00:00
|
|
|
}
|
|
|
|
}
|