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
106 changes: 0 additions & 106 deletions app/Console/Commands/BackfillAlbumFields.php

This file was deleted.

107 changes: 98 additions & 9 deletions app/Console/Commands/RecomputeAlbumStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,38 @@ class RecomputeAlbumStats extends Command
* @var string
*/
protected $signature = 'lychee:recompute-album-stats
{album_id : The ID of the album to recompute}
{--sync : Run synchronously instead of dispatching a job}';
{album_id? : Optional album ID for single-album mode}
{--sync : Run synchronously (single-album mode only)}
{--dry-run : Preview without making changes (bulk mode only)}
{--chunk=1000 : Number of albums to process per batch (bulk mode only)}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Manually recompute stats for a specific album (useful for recovery after propagation failures)';
protected $description = 'Recompute album stats. With album_id: recompute single album. Without album_id: bulk backfill all albums.';

/**
* Execute the console command.
*/
public function handle(): int
{
$album_id = $this->argument('album_id');

// Dual behavior: with album_id = single-album mode, without = bulk mode
if ($album_id !== null) {
return $this->handleSingleAlbum($album_id);
}

return $this->handleBulkBackfill();
}

/**
* Handle single-album recompute mode.
*/
private function handleSingleAlbum(string $album_id): int
{
$sync = $this->option('sync');

// Validate album exists
Expand Down Expand Up @@ -66,15 +82,88 @@ public function handle(): int

return Command::FAILURE;
}
} else {
// Dispatch job to queue
RecomputeAlbumStatsJob::dispatch($album_id);
}

// Dispatch job to queue
RecomputeAlbumStatsJob::dispatch($album_id);

$this->info('✓ Job dispatched to queue');
$this->info(' Note: Stats will be updated when the queue worker processes the job');
Log::info("Manual recompute job dispatched for album {$album_id}");

return Command::SUCCESS;
}

/**
* Handle bulk backfill mode for all albums.
*/
private function handleBulkBackfill(): int
{
$dry_run = $this->option('dry-run');
$chunk_size = (int) $this->option('chunk');

$this->info('✓ Job dispatched to queue');
$this->info(' Note: Stats will be updated when the queue worker processes the job');
Log::info("Manual recompute job dispatched for album {$album_id}");
if ($chunk_size < 1) {
$this->error('Chunk size must be at least 1');

return Command::FAILURE;
}

$this->info('Starting album fields backfill...');
if ($dry_run) {
$this->warn('DRY RUN MODE - No changes will be made');
}

// Get total count
$total = Album::query()->count();
$this->info("Found {$total} albums to process");

if ($total === 0) {
$this->info('No albums to process');

return Command::SUCCESS;
}

// Process albums ordered by _lft ASC (leaf-to-root order)
// This ensures child albums are computed before parents
$bar = $this->output->createProgressBar($total);
$bar->start();

$processed = 0;

Album::query()
->orderBy('_lft', 'asc')
->chunk($chunk_size, function (\Illuminate\Database\Eloquent\Collection $albums) use ($dry_run, &$processed, $bar): void {
/** @var Album $album */
foreach ($albums as $album) {
if (!$dry_run) {
// Dispatch job to recompute stats for this album
RecomputeAlbumStatsJob::dispatch($album->id);
}

$processed++;
$bar->advance();

// Log progress at intervals
if ($processed % 100 === 0) {
$percentage = round(($processed / $bar->getMaxSteps()) * 100, 2);
Log::info("Backfilled {$processed}/{$bar->getMaxSteps()} albums ({$percentage}%)");
}
}
});

$bar->finish();
$this->newLine(2);

if ($dry_run) {
$this->info("DRY RUN: Would have dispatched {$processed} jobs");
} else {
$this->info("Dispatched {$processed} jobs to recompute album stats");
$this->info('Jobs will be processed by the queue worker');
$this->warn('Note: This operation may take some time for large galleries');
}

Log::info("Backfill completed: {$processed} albums processed");

return Command::SUCCESS;
}
}
20 changes: 11 additions & 9 deletions app/Http/Controllers/Admin/Maintenance/FulfillPreCompute.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,9 @@ class FulfillPreCompute extends Controller
*/
public function do(MaintenanceRequest $request): void
{
$queue_connection = Config::get('queue.default', 'sync');
$is_sync = $queue_connection === 'sync';
$is_sync = Config::get('queue.default', 'sync') === 'sync';

$query = $this->getAlbumsNeedingComputation()
->whereRaw('_lft = _rgt - 1') // Only leaf albums
->orderBy('_lft', 'desc');

if ($is_sync) {
Expand Down Expand Up @@ -109,11 +107,15 @@ public function check(MaintenanceRequest $request): int
private function getAlbumsNeedingComputation(): Builder
{
return Album::query()
->whereNull('max_taken_at')
->whereNull('min_taken_at')
->where('num_children', 0)
->where('num_photos', 0)
->whereNull('auto_cover_id_max_privilege')
->whereNull('auto_cover_id_least_privilege');
->where(fn (Builder $q) => $q
->whereNull('max_taken_at')
->whereNull('min_taken_at')
->where('num_children', 0)
->where('num_photos', 0)
)
->orWhere(fn (Builder $q) => $q
->whereNull('auto_cover_id_max_privilege')
->whereNull('auto_cover_id_least_privilege')
);
}
}
72 changes: 35 additions & 37 deletions app/Jobs/RecomputeAlbumStatsJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,43 +111,41 @@ public function handle(): void
}

try {
DB::transaction(function (): void {
// Fetch the album.
$album = Album::where('id', '=', $this->album_id)->first();
if ($album === null) {
Log::warning("Album {$this->album_id} not found, skipping recompute.");

return;
}

$is_nsfw_context = DB::table('albums')
->leftJoin('base_albums as base', 'albums.id', '=', 'base.id')
->where('base.is_nsfw', '=', true)
->where('albums._lft', '<=', $album->_lft)
->where('albums._rgt', '>=', $album->_rgt)
->count() > 0;

// Compute counts
$album->num_children = $this->computeNumChildren($album);
$album->num_photos = $this->computeNumPhotos($album);

// Compute date range
$dates = $this->computeTakenAtRange($album);
$album->min_taken_at = $dates['min'];
$album->max_taken_at = $dates['max'];

// Compute cover IDs (simplified for now - will be enhanced in I3)
$album->auto_cover_id_max_privilege = $this->computeMaxPrivilegeCover($album, $is_nsfw_context);
$album->auto_cover_id_least_privilege = $this->computeLeastPrivilegeCover($album, $is_nsfw_context);
Log::debug("Computed covers for album {$album->id}: max_privilege=" . ($album->auto_cover_id_max_privilege ?? 'null') . ', least_privilege=' . ($album->auto_cover_id_least_privilege ?? 'null'));
$album->save();

// Propagate to parent if exists
if ($album->parent_id !== null) {
Log::debug("Propagating to parent {$album->parent_id}");
self::dispatch($album->parent_id);
}
});
// Fetch the album.
$album = Album::where('id', '=', $this->album_id)->first();
if ($album === null) {
Log::warning("Album {$this->album_id} not found, skipping recompute.");

return;
}

$is_nsfw_context = DB::table('albums')
->leftJoin('base_albums as base', 'albums.id', '=', 'base.id')
->where('base.is_nsfw', '=', true)
->where('albums._lft', '<=', $album->_lft)
->where('albums._rgt', '>=', $album->_rgt)
->count() > 0;

// Compute counts
$album->num_children = $this->computeNumChildren($album);
$album->num_photos = $this->computeNumPhotos($album);

// Compute date range
$dates = $this->computeTakenAtRange($album);
$album->min_taken_at = $dates['min'];
$album->max_taken_at = $dates['max'];

// Compute cover IDs (simplified for now - will be enhanced in I3)
$album->auto_cover_id_max_privilege = $this->computeMaxPrivilegeCover($album, $is_nsfw_context);
$album->auto_cover_id_least_privilege = $this->computeLeastPrivilegeCover($album, $is_nsfw_context);
Log::debug("Computed covers for album {$album->id}: max_privilege=" . ($album->auto_cover_id_max_privilege ?? 'null') . ', least_privilege=' . ($album->auto_cover_id_least_privilege ?? 'null'));
$album->save();

// Propagate to parent if exists
if ($album->parent_id !== null) {
Log::debug("Propagating to parent {$album->parent_id}");
self::dispatch($album->parent_id);
}
} catch (\Exception $e) {
Log::error("Propagation stopped at album {$this->album_id} due to failure: " . $e->getMessage());

Expand Down
10 changes: 10 additions & 0 deletions database/factories/AccessPermissionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public function definition(): array
];
}

/**
* Make the AccessPermission public (i.e., not tied to any user or user group).
* However, by default, it still requires a link to access. Use ->visible() to
* make it actually visible in listings.
*
* This has an impact on how thumbs are computed.
*/
public function public()
{
return $this->state(function (array $attributes) {
Expand Down Expand Up @@ -133,6 +140,9 @@ public function grants_full_photo()
});
}

/**
* Make the AccessPermission not require a link: actually visible in listings.
*/
public function visible()
{
return $this->state(function (array $attributes) {
Expand Down
Loading
Loading