1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace App\Console\Commands; |
4
|
|
|
|
5
|
|
|
use App\Models\Release; |
6
|
|
|
use Illuminate\Console\Command; |
7
|
|
|
use Illuminate\Support\Facades\Artisan; |
8
|
|
|
|
9
|
|
|
class FetchSamples extends Command |
10
|
|
|
{ |
11
|
|
|
protected $signature = 'releases:fetch-samples |
12
|
|
|
{--category= : Category id or comma-separated list of category ids (required)} |
13
|
|
|
{--limit=0 : Max number of releases to process (0 = all)} |
14
|
|
|
{--chunk=500 : Chunk size when iterating releases} |
15
|
|
|
{--dry-run : Show how many and which GUIDs would be processed without running} |
16
|
|
|
{--show-output : Display output from each releases:additional invocation}'; |
17
|
|
|
|
18
|
|
|
protected $description = 'Fetch/generate samples by running additional postprocessing (with reset) for releases in the supplied category / categories having jpgstatus = 0.'; |
19
|
|
|
|
20
|
|
|
public function handle(): int |
21
|
|
|
{ |
22
|
|
|
$limit = (int) $this->option('limit'); |
23
|
|
|
$chunkSize = (int) $this->option('chunk'); |
24
|
|
|
$dryRun = (bool) $this->option('dry-run'); |
25
|
|
|
$showOutput = (bool) $this->option('show-output'); |
26
|
|
|
$categoryOpt = $this->option('category'); |
27
|
|
|
|
28
|
|
|
// Validate the required category option. |
29
|
|
|
if ($categoryOpt === null || trim((string) $categoryOpt) === '') { |
30
|
|
|
$this->info('Category option is empty. Provide --category with one or more numeric category IDs. Command will not run.'); |
31
|
|
|
|
32
|
|
|
return self::SUCCESS; |
33
|
|
|
} |
34
|
|
|
|
35
|
|
|
// Parse categories: allow comma or whitespace separated values. |
36
|
|
|
$catIds = collect(preg_split('/[\s,]+/', trim((string) $categoryOpt), -1, PREG_SPLIT_NO_EMPTY)) |
|
|
|
|
37
|
|
|
->map(fn ($v) => trim($v)) |
38
|
|
|
->filter(fn ($v) => ctype_digit($v)) |
39
|
|
|
->map(fn ($v) => (int) $v) |
40
|
|
|
->unique() |
41
|
|
|
->values(); |
42
|
|
|
|
43
|
|
|
if ($catIds->isEmpty()) { |
44
|
|
|
$this->info('Category option provided but no valid numeric IDs parsed. Command will not run.'); |
45
|
|
|
|
46
|
|
|
return self::SUCCESS; |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
if ($limit < 0) { |
50
|
|
|
$this->error('Limit must be >= 0'); |
51
|
|
|
|
52
|
|
|
return self::FAILURE; |
53
|
|
|
} |
54
|
|
|
if ($chunkSize < 1) { |
55
|
|
|
$this->error('Chunk size must be >= 1'); |
56
|
|
|
|
57
|
|
|
return self::FAILURE; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
// Build base query now |
61
|
|
|
$baseQuery = Release::query() |
62
|
|
|
->whereIn('categories_id', $catIds->all()) |
63
|
|
|
->where('jpgstatus', 0) |
64
|
|
|
->orderBy('id'); |
|
|
|
|
65
|
|
|
|
66
|
|
|
$totalAll = $baseQuery->count(); |
67
|
|
|
if ($totalAll === 0) { |
68
|
|
|
$this->info('No matching releases found (categories_id IN ['.implode(',', $catIds->all()).'] AND jpgstatus = 0).'); |
69
|
|
|
|
70
|
|
|
return self::SUCCESS; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
$effectiveTotal = $limit > 0 ? min($limit, $totalAll) : $totalAll; |
74
|
|
|
$this->info('Categories: ['.implode(',', $catIds->all()).']'); |
75
|
|
|
$this->info("Found {$totalAll} matching release(s). Processing {$effectiveTotal}.".($dryRun ? ' (dry-run)' : '')); |
76
|
|
|
|
77
|
|
|
if ($dryRun) { |
78
|
|
|
$previewQuery = clone $baseQuery; |
79
|
|
|
if ($limit > 0) { |
80
|
|
|
$previewQuery->limit($limit); |
81
|
|
|
} |
82
|
|
|
$previewGuids = $previewQuery->pluck('guid'); |
83
|
|
|
$this->line('Dry run: GUIDs to process (with --reset):'); |
84
|
|
|
foreach ($previewGuids as $g) { |
85
|
|
|
$this->line($g); |
86
|
|
|
} |
87
|
|
|
$this->info('Dry run complete.'); |
88
|
|
|
|
89
|
|
|
return self::SUCCESS; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
$processed = 0; |
93
|
|
|
$failed = 0; |
94
|
|
|
$remaining = $effectiveTotal; |
95
|
|
|
|
96
|
|
|
$bar = $this->output->createProgressBar($effectiveTotal); |
|
|
|
|
97
|
|
|
$bar->start(); |
98
|
|
|
|
99
|
|
|
$query = clone $baseQuery; |
100
|
|
|
|
101
|
|
|
$query->chunkById($chunkSize, function ($releases) use (&$processed, &$failed, &$remaining, $bar, $showOutput) { |
102
|
|
|
foreach ($releases as $release) { |
103
|
|
|
if ($remaining <= 0) { |
104
|
|
|
return false; // stop chunking |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
$guid = $release->guid; |
108
|
|
|
try { |
109
|
|
|
// Call the existing single-release additional processing command with this GUID. |
110
|
|
|
$exitCode = Artisan::call('releases:additional', [ |
111
|
|
|
'guid' => $guid, // pass GUID explicitly |
112
|
|
|
'--reset' => true, |
113
|
|
|
]); |
114
|
|
|
$subOutput = trim(Artisan::output()); |
115
|
|
|
|
116
|
|
|
if ($exitCode === 0) { |
117
|
|
|
$processed++; |
118
|
|
|
if ($showOutput && $subOutput !== '') { |
119
|
|
|
$this->getOutput()->writeln("\n<info>{$guid}</info> -> {$subOutput}"); |
120
|
|
|
} |
121
|
|
|
} else { |
122
|
|
|
$failed++; |
123
|
|
|
$this->getOutput()->writeln("\n<error>Non-zero exit code ({$exitCode}) for GUID {$guid}</error>".($showOutput && $subOutput !== '' ? "\n Output: {$subOutput}" : '')); |
124
|
|
|
} |
125
|
|
|
} catch (\Throwable $e) { |
126
|
|
|
$failed++; |
127
|
|
|
$this->getOutput()->writeln("\n<error>Error processing GUID {$guid}: {$e->getMessage()}</error>"); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
$remaining--; |
131
|
|
|
$bar->advance(); |
132
|
|
|
|
133
|
|
|
if ($remaining <= 0) { |
134
|
|
|
break; // exit foreach to stop further work |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
if ($remaining <= 0) { |
139
|
|
|
return false; // signal chunkById to stop |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
return true; // continue chunking |
143
|
|
|
}, 'id'); |
144
|
|
|
|
145
|
|
|
$bar->finish(); |
146
|
|
|
$this->newLine(); |
147
|
|
|
$this->info("Processing complete. Success: {$processed}, Failed: {$failed}."); |
148
|
|
|
|
149
|
|
|
return $failed === 0 ? self::SUCCESS : self::FAILURE; |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
|