diff --git a/app/Console/Commands/GenerateFeaturedArtworkThumbnailsCommand.php b/app/Console/Commands/GenerateFeaturedArtworkThumbnailsCommand.php new file mode 100644 index 00000000..9302497f --- /dev/null +++ b/app/Console/Commands/GenerateFeaturedArtworkThumbnailsCommand.php @@ -0,0 +1,189 @@ +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 $artworkIds + * @return LazyCollection + */ + 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 + */ + private function orderedCursor(Builder $query): LazyCollection + { + return $query + ->orderByDesc('artworks.id') + ->cursor(); + } +} diff --git a/app/Jobs/GenerateFeaturedArtworkThumbnailsJob.php b/app/Jobs/GenerateFeaturedArtworkThumbnailsJob.php new file mode 100644 index 00000000..eb96fe19 --- /dev/null +++ b/app/Jobs/GenerateFeaturedArtworkThumbnailsJob.php @@ -0,0 +1,56 @@ +onQueue($queue); + } + } + + public function handle( + FeaturedArtworkThumbnailGenerator $generator, + ): void { + $artwork = Artwork::withTrashed()->find($this->artworkId); + + if (! $artwork instanceof Artwork) { + return; + } + + $result = $generator->generate($artwork, $this->force); + + if (($result['failed'] ?? []) !== []) { + Log::warning('Featured artwork thumbnail generation had partial failures', [ + 'artwork_id' => $artwork->id, + 'failed_variants' => array_keys((array) $result['failed']), + ]); + } + } +}