190 lines
6.2 KiB
PHP
190 lines
6.2 KiB
PHP
<?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();
|
|
}
|
|
}
|