Add job and artisan command for generating featured thumbnails
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\GenerateFeaturedArtworkThumbnailsJob;
|
||||
use App\Models\Artwork;
|
||||
use App\Services\Featured\FeaturedArtworkSelector;
|
||||
use App\Services\Images\FeaturedArtworkThumbnailGenerator;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
|
||||
final class GenerateFeaturedArtworkThumbnailsCommand extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:featured-thumbnails:generate
|
||||
{--artwork=* : Restrict generation to one or more artwork IDs}
|
||||
{--only-featured : Restrict generation to currently selected featured artworks}
|
||||
{--missing-only : Only generate artworks missing at least one featured variant}
|
||||
{--all : Process all artworks with a hash and source extension}
|
||||
{--limit=0 : Cap the number of artworks processed}
|
||||
{--queue : Dispatch background jobs instead of generating inline}
|
||||
{--force : Regenerate all featured variants even when they already exist}
|
||||
{--dry-run : Report planned generation without writing files}';
|
||||
|
||||
protected $description = 'Generate dedicated featured artwork CDN thumbnails for the homepage hero';
|
||||
|
||||
public function __construct(
|
||||
private readonly FeaturedArtworkSelector $selector,
|
||||
private readonly FeaturedArtworkThumbnailGenerator $generator,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$artworkIds = collect((array) $this->option('artwork'))
|
||||
->map(static fn (mixed $id): int => (int) $id)
|
||||
->filter(static fn (int $id): bool => $id > 0)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$force = (bool) $this->option('force');
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$queue = (bool) $this->option('queue');
|
||||
$limit = max(0, (int) $this->option('limit'));
|
||||
$all = (bool) $this->option('all');
|
||||
$explicitOnlyFeatured = (bool) $this->option('only-featured');
|
||||
$missingOnly = $force ? false : ((bool) $this->option('missing-only') || ($artworkIds === [] && ! $all));
|
||||
|
||||
if ($all && $explicitOnlyFeatured) {
|
||||
$this->error('Use either --all or --only-featured, not both.');
|
||||
|
||||
return self::INVALID;
|
||||
}
|
||||
|
||||
if ($queue && $dryRun) {
|
||||
$this->error('Use either --queue or --dry-run, not both.');
|
||||
|
||||
return self::INVALID;
|
||||
}
|
||||
|
||||
$onlyFeatured = $artworkIds === [] && ! $all;
|
||||
if ($explicitOnlyFeatured) {
|
||||
$onlyFeatured = true;
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$queued = 0;
|
||||
$generatedVariants = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($this->candidateArtworks($artworkIds, $onlyFeatured) as $artwork) {
|
||||
if ($limit > 0 && $processed >= $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
$processed++;
|
||||
|
||||
$plan = $this->generator->plan($artwork, $force);
|
||||
$targetVariants = (array) ($plan['target_variants'] ?? []);
|
||||
|
||||
if ($missingOnly && ! $force && $targetVariants === []) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line(sprintf(
|
||||
'[dry-run] artwork=%d variants=%s',
|
||||
(int) $artwork->id,
|
||||
$targetVariants === [] ? 'none' : implode(',', $targetVariants),
|
||||
));
|
||||
|
||||
if ($targetVariants === []) {
|
||||
$skipped++;
|
||||
} else {
|
||||
$generatedVariants += count($targetVariants);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($queue) {
|
||||
GenerateFeaturedArtworkThumbnailsJob::dispatch((int) $artwork->id, $force);
|
||||
$queued++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->generator->generate($artwork, $force);
|
||||
|
||||
$generatedVariants += (int) ($result['generated'] ?? 0);
|
||||
$skipped += count((array) ($result['target_variants'] ?? [])) === 0 ? 1 : 0;
|
||||
|
||||
if (($result['failed'] ?? []) !== []) {
|
||||
$failed++;
|
||||
$this->warn(sprintf(
|
||||
'Artwork %d failed for variants: %s',
|
||||
(int) $artwork->id,
|
||||
implode(', ', array_keys((array) $result['failed'])),
|
||||
));
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$failed++;
|
||||
$this->warn(sprintf('Artwork %d failed: %s', (int) $artwork->id, $exception->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
$mode = $dryRun ? 'dry-run' : ($queue ? 'queued' : 'generated');
|
||||
|
||||
$this->info(sprintf(
|
||||
'Featured artwork thumbnail %s complete: processed=%d queued=%d generated_variants=%d skipped=%d failed=%d',
|
||||
$mode,
|
||||
$processed,
|
||||
$queued,
|
||||
$generatedVariants,
|
||||
$skipped,
|
||||
$failed,
|
||||
));
|
||||
|
||||
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $artworkIds
|
||||
* @return LazyCollection<int, Artwork>
|
||||
*/
|
||||
private function candidateArtworks(array $artworkIds, bool $onlyFeatured): LazyCollection
|
||||
{
|
||||
if ($artworkIds !== []) {
|
||||
return Artwork::query()
|
||||
->withTrashed()
|
||||
->whereIn('id', $artworkIds)
|
||||
->whereNotNull('hash')
|
||||
->where('hash', '!=', '')
|
||||
->whereNotNull('file_ext')
|
||||
->where('file_ext', '!=', '')
|
||||
->orderByDesc('id')
|
||||
->cursor();
|
||||
}
|
||||
|
||||
$query = $onlyFeatured
|
||||
? $this->selector->querySelectedArtworks()
|
||||
: Artwork::query()
|
||||
->select('artworks.*')
|
||||
->withTrashed()
|
||||
->whereNotNull('hash')
|
||||
->where('hash', '!=', '')
|
||||
->whereNotNull('file_ext')
|
||||
->where('file_ext', '!=', '');
|
||||
|
||||
return $this->orderedCursor($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LazyCollection<int, Artwork>
|
||||
*/
|
||||
private function orderedCursor(Builder $query): LazyCollection
|
||||
{
|
||||
return $query
|
||||
->orderByDesc('artworks.id')
|
||||
->cursor();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user