<?php
namespace App\Builders;
use App\Builders\Concerns\CanScopeByUser;
use App\Facades\License;
use App\Models\Album;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\DB;
use LogicException;
use Webmozart\Assert\Assert;
class AlbumBuilder extends FavoriteableBuilder
{
use CanScopeByUser;
public const SORT_COLUMNS_NORMALIZE_MAP = [
'name' => 'albums.name',
'year' => 'albums.year',
'created_at' => 'albums.created_at',
'artist_name' => 'albums.artist_name',
];
private const VALID_SORT_COLUMNS = [
'albums.name',
'albums.year',
'albums.created_at',
'albums.artist_name',
'favorite', // alias column for favorite status
];
public function onlyStandard(): self
{
return $this->whereNot('albums.name', Album::UNKNOWN_NAME);
}
private function accessible(): self
{
if (License::isCommunity()) {
// With the Community license, all albums are accessible by all users.
return $this;
}
throw_unless($this->user, new LogicException('User must be set to query accessible albums.'));
if (!$this->user->preferences->includePublicMedia) {
// If the user does not want to include public media, we only return albums
// that belong to them.
return $this->whereBelongsTo($this->user);
}
// otherwise, we return albums that belong to the user or
// albums that have at least one public song owned by the user in the same organization.
return $this->where(function (Builder $query): void {
$query->whereBelongsTo($this->user)
->orWhereHas('songs', function (Builder $q): void {
$q->where('songs.is_public', true)
->whereHas('owner', function (Builder $owner): void {
$owner->where('organization_id', $this->user->organization_id)
->where('owner_id', '<>', $this->user->id);
});
});
});
}
private function withPlayCount($includingFavoriteStatus = false): self
{
throw_unless($this->user, new LogicException('User must be set to query play counts.'));
$groupColumns = $includingFavoriteStatus
? ['albums.id', 'favorites.created_at']
: ['albums.id'];
// As we might have joined the `songs` table already, use an alias for the `songs` table
// in this join to avoid conflicts.
return $this->leftJoin('songs as songs_for_playcount', 'albums.id', 'songs_for_playcount.album_id')
->join('interactions', function (JoinClause $join): void {
$join->on('songs_for_playcount.id', 'interactions.song_id')
->where('interactions.user_id', $this->user->id);
})
->groupBy($groupColumns)
->addSelect(DB::raw("SUM(interactions.play_count) as play_count"));
}
public function withUserContext(
User $user,
bool $includeFavoriteStatus = true,
bool $favoritesOnly = false,
bool $includePlayCount = false,
): self {
$this->user = $user;
return $this->accessible()
->when($includeFavoriteStatus, static fn (self $query) => $query->withFavoriteStatus($favoritesOnly))
->when($includePlayCount, static fn (self $query) => $query->withPlayCount($includeFavoriteStatus));
}
private static function normalizeSortColumn(string $column): string
{
return array_key_exists($column, self::SORT_COLUMNS_NORMALIZE_MAP)
? self::SORT_COLUMNS_NORMALIZE_MAP[$column]
: $column;
}
public function sort(string $column, string $direction): self
{
$column = self::normalizeSortColumn($column);
Assert::oneOf($column, self::VALID_SORT_COLUMNS);
Assert::oneOf(strtolower($direction), ['asc', 'desc']);
return $this
->orderBy($column, $direction)
// Depending on the column, we might need to order by the album's name as well.
->when($column === 'albums.artist_name', static fn (self $query) => $query->orderBy('albums.name'))
->when($column === 'albums.year', static fn (self $query) => $query->orderBy('albums.name'))
->when($column === 'favorite', static fn (self $query) => $query->orderBy('albums.name'));
}
}