Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/Actions/Album/Delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function do(array $album_ids): FileDeleter
/** @var Collection<int,Album> $albums */
/** @phpstan-ignore varTag.type (False positive, NestedSetCollection requires Eloquent Collection) */
$albums = Album::query()
->without(['cover', 'thumb'])
->without(['cover', 'thumb', 'min_privilege_cover', 'max_privilege_cover'])
->select(['id', 'parent_id', '_lft', '_rgt', 'track_short_path'])
->findMany($album_ids);

Expand All @@ -100,7 +100,7 @@ public function do(array $album_ids): FileDeleter
/** @var Album $album */
foreach ($albums as $album) {
// Collect all (aka recursive) sub-albums in each album
$sub_albums = $album->descendants()->getQuery()->without(['cover', 'thumb'])->select(['id', 'track_short_path'])->get();
$sub_albums = $album->descendants()->getQuery()->without(['cover', 'thumb', 'min_privilege_cover', 'max_privilege_cover'])->select(['id', 'track_short_path'])->get();
$recursive_album_ids = array_merge($recursive_album_ids, $sub_albums->pluck('id')->all());
$recursive_album_tracks = $recursive_album_tracks->merge($sub_albums->pluck('track_short_path'));
}
Expand Down
8 changes: 7 additions & 1 deletion app/Actions/Albums/Flow.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,13 @@ private function getQuery(Album|null $base, bool $with_relations): AlbumBuilder

$base_query = Album::query();
if ($with_relations) {
$base_query->with(['cover', 'cover.size_variants', 'statistics', 'photos', 'photos.statistics', 'photos.size_variants', 'photos.palette', 'photos.tags', 'photos.rating']);
$base_query->with([
'cover', 'cover.size_variants',
'max_privilege_cover', 'max_privilege_cover.size_variants',
'min_privilege_cover', 'min_privilege_cover.size_variants',
'statistics',
'photos',
'photos.statistics', 'photos.size_variants', 'photos.palette', 'photos.tags', 'photos.rating']);
}

// Only join what we need for ordering.
Expand Down
2 changes: 1 addition & 1 deletion app/Actions/User/Notify.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function do(Photo $photo): void
// Admin user is always notified
$users = User::query()->where('may_administrate', '=', true)->get();

$albums = Album::query()->without(['thumbs', 'statistics', 'cover'])->join(PA::PHOTO_ALBUM, PA::ALBUM_ID, '=', 'album.id')
$albums = Album::query()->without(['thumbs', 'statistics', 'cover', 'min_privilege_cover', 'max_privilege_cover'])->join(PA::PHOTO_ALBUM, PA::ALBUM_ID, '=', 'album.id')
->where(PA::PHOTO_ID, '=', $photo->id)
->get();

Expand Down
29 changes: 28 additions & 1 deletion app/Models/Album.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
* @property Carbon|null $max_taken_at Maximum taken_at timestamp of all photos in album and descendants.
* @property Carbon|null $min_taken_at Minimum taken_at timestamp of all photos in album and descendants.
* @property string|null $auto_cover_id_max_privilege Automatically selected cover photo ID (admin/owner view).
* @property Photo|null $max_privilege_cover
* @property string|null $auto_cover_id_least_privilege Automatically selected cover photo ID (most restrictive view).
* @property Photo|null $min_privilege_cover
* @property LicenseType $license
* @property string|null $cover_id
* @property Photo|null $cover
Expand Down Expand Up @@ -201,7 +203,12 @@ class Album extends BaseAlbum implements Node
/**
* The relationships that should always be eagerly loaded by default.
*/
protected $with = ['cover', 'cover.size_variants', 'thumb'];
protected $with = [
'cover', 'cover.size_variants',
'min_privilege_cover', 'min_privilege_cover.size_variants',
'max_privilege_cover', 'max_privilege_cover.size_variants',
'thumb',
];

/**
* Return the relationship between this album and photos which are
Expand Down Expand Up @@ -265,6 +272,26 @@ public function cover(): HasOne
return $this->hasOne(Photo::class, 'id', 'cover_id');
}

/**
* Return the relationship between an album and its min-privilege cover.
*
* @return HasOne<Photo,$this>
*/
public function min_privilege_cover(): HasOne
{
return $this->hasOne(Photo::class, 'id', 'auto_cover_id_least_privilege');
}

/**
* Return the relationship between an album and its max-privilege cover.
*
* @return HasOne<Photo,$this>
*/
public function max_privilege_cover(): HasOne
{
return $this->hasOne(Photo::class, 'id', 'auto_cover_id_max_privilege');
}

/**
* Return the relationship between an album and its header.
*
Expand Down
117 changes: 52 additions & 65 deletions app/Relations/HasAlbumThumb.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;

/**
Expand Down Expand Up @@ -65,34 +64,50 @@ protected function getRelationQuery(): FixedQueryBuilder
}

/**
* Select the appropriate cover ID based on user privileges.
*
* Priority:
* 1. Explicit cover_id (if set)
* 2. auto_cover_id_max_privilege (if admin or owns album/ancestor)
* 3. auto_cover_id_least_privilege (public view)
* Determine the cover type to use for the album.
*
* @param Album $album
*
* @return string|null
* @return string
*/
protected function selectCoverIdForAlbum(Album $album): ?string
protected function getCoverTypeForAlbum(Album $album): string
{
// Priority 1: Explicit cover
if ($album->cover_id !== null) {
return $album->cover_id;
return 'cover_id';
}

/** @var ?User $user */
$user = Auth::user();

// Priority 2: Max-privilege cover for admin or owner
if ($user?->may_administrate === true || $album->owner_id === $user?->id) {
return $album->auto_cover_id_max_privilege;
return 'auto_cover_id_max_privilege';
}

// Priority 3: Least-privilege cover for public
return $album->auto_cover_id_least_privilege;
return 'auto_cover_id_least_privilege';
}

/**
* Select the appropriate cover ID based on user privileges.
*
* Priority:
* 1. Explicit cover_id (if set)
* 2. auto_cover_id_max_privilege (if admin or owns album/ancestor)
* 3. auto_cover_id_least_privilege (public view)
*
* @param Album $album
*
* @return string|null
*/
protected function selectCoverIdForAlbum(Album $album): ?string
{
return match ($this->getCoverTypeForAlbum($album)) {
'cover_id' => $album->cover_id,
'auto_cover_id_max_privilege' => $album->auto_cover_id_max_privilege,
'auto_cover_id_least_privilege' => $album->auto_cover_id_least_privilege,
default => null,
};
}

/**
Expand Down Expand Up @@ -130,42 +145,17 @@ public function addConstraints(): void
}

/**
* Builds a query to eagerly load the thumbnails of a sequence of albums.
*
* Now uses pre-computed cover IDs (auto_cover_id_max_privilege and
* auto_cover_id_least_privilege) instead of expensive runtime queries.
* We do not eager load any covers.
* This relation is only meaningful for single albums.
* In case of multiple album we use the preloaded values from
* `cover_id`, `auto_cover_id_max_privilege`, and `auto_cover_id_least_privilege`.
*
* @param array<Album> $models
*/
public function addEagerConstraints(array $models): void
{
// Build mapping of album_id => cover_id for each album
$album_to_cover = [];
foreach ($models as $album) {
$cover_id = $this->selectCoverIdForAlbum($album);
if ($cover_id !== null) {
$album_to_cover[$album->id] = $cover_id;
}
}

if (count($album_to_cover) === 0) {
// No covers to load - make query return empty result
$this->getRelationQuery()->whereRaw('1 = 0');

return;
}

// Select photos with their album association
$this->getRelationQuery()
->select([
'photos.id as id',
'photos.type as type',
DB::raw('CASE ' .
collect($album_to_cover)->map(fn ($cover_id, $album_id) => "WHEN photos.id = '$cover_id' THEN '$album_id'"
)->implode(' ') .
' END as covered_album_id'),
])
->whereIn('photos.id', array_values($album_to_cover));
// No covers to load - make query return empty result
$this->getRelationQuery()->whereRaw('1 = 0');
}

/**
Expand Down Expand Up @@ -194,18 +184,17 @@ public function initRelation(array $models, $relation): array
*/
public function match(array $models, Collection $results, $relation): array
{
$dictionary = $results->mapToDictionary(function ($result) {
/** @var Photo&object{covered_album_id: string} $result */
return [$result->covered_album_id => $result];
})->all();

// Match photos to albums using the covered_album_id
/** @var Album $album */
foreach ($models as $album) {
$album_id = $album->id;
if (isset($dictionary[$album_id])) {
$cover = reset($dictionary[$album_id]);
$album->setRelation($relation, Thumb::createFromPhoto($cover));
$cover_type = $this->getCoverTypeForAlbum($album);
if ($cover_type === 'cover_id' && $album->cover_id !== null) {
// We do not need to do anything here, because we already have the cover
// loaded via the `cover` relation of `Album`.
$album->setRelation($relation, Thumb::createFromPhoto($album->cover));
} elseif ($cover_type === 'auto_cover_id_max_privilege' && $album->auto_cover_id_max_privilege !== null) {
$album->setRelation($relation, Thumb::createFromPhoto($album->max_privilege_cover));
} elseif ($cover_type === 'auto_cover_id_least_privilege' && $album->auto_cover_id_least_privilege !== null) {
$album->setRelation($relation, Thumb::createFromPhoto($album->min_privilege_cover));
} else {
$album->setRelation($relation, null);
}
Expand All @@ -226,21 +215,19 @@ public function getResults(): ?Thumb
// is always eagerly loaded with its cover and hence, we already
// have it.
// See {@link Album::with}
$cover_id = $this->selectCoverIdForAlbum($album);
if ($cover_id === $album->cover_id) {
$cover_type = $this->getCoverTypeForAlbum($album);
if ($cover_type === 'cover_id' && $album->cover_id !== null) {
// We do not need to do anything here, because we already have the cover
// loaded via the `cover` relation of `Album`.
return Thumb::createFromPhoto($album->cover);
}

if ($cover_id !== null) {
// Use pre-computed cover ID (explicit, max-privilege, or least-privilege)
$photo = Photo::query()->with(['size_variants' => (fn ($r) => Thumb::sizeVariantsFilter($r))])->find($cover_id);

return Thumb::createFromPhoto($photo);
} elseif ($cover_type === 'auto_cover_id_max_privilege' && $album->auto_cover_id_max_privilege !== null) {
return Thumb::createFromPhoto($album->max_privilege_cover);
} elseif ($cover_type === 'auto_cover_id_least_privilege' && $album->auto_cover_id_least_privilege !== null) {
return Thumb::createFromPhoto($album->min_privilege_cover);
} else {
// Fallback to legacy query if no cover available
return Thumb::createFromQueryable(
$this->getRelationQuery(),
$this->sorting,
$this->sorting
);
}
}
Expand Down
Loading
Loading