Passed
Push — master ( 59225e...ef513e )
by Darko
11:58
created

RenameOtherMiscReleases::matchByTitleAndSize()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 50
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 26
c 3
b 0
f 0
dl 0
loc 50
rs 6.2666
cc 14
nc 37
nop 6

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace App\Console\Commands;
4
5
use App\Models\Category;
6
use App\Models\Predb;
7
use App\Models\Release;
8
use Blacklight\Categorize;
9
use Blacklight\ColorCLI;
10
use Blacklight\ElasticSearchSiteSearch;
11
use Blacklight\ManticoreSearch;
12
use Illuminate\Console\Command;
13
14
class RenameOtherMiscReleases extends Command
15
{
16
    /**
17
     * The name and signature of the console command.
18
     *
19
     * @var string
20
     */
21
    protected $signature = 'releases:rename-other-misc
22
                                 {--limit= : Maximum number of releases to process}
23
                                 {--dry-run : Show what would be renamed without actually updating}
24
                                 {--show : Display detailed release changes}
25
                                 {--size-tolerance=5 : Size tolerance percentage for matching (default: 5%)}';
26
27
    /**
28
     * The console command description.
29
     *
30
     * @var string
31
     */
32
    protected $description = 'Rename releases in other->misc and other-hashed categories using PreDB entries';
33
34
    protected ?ColorCLI $colorCLI = null;
35
36
    protected int $renamed = 0;
37
38
    protected int $checked = 0;
39
40
    protected int $matched = 0;
41
42
    /**
43
     * Cache for category lookups to avoid repeated DB queries.
44
     */
45
    protected array $categoryCache = [];
46
47
    /**
48
     * Execute the console command.
49
     */
50
    public function handle(): int
51
    {
52
        $this->colorCLI = new ColorCLI;
53
        $categorize = new Categorize;
54
55
        $limit = $this->option('limit');
56
        $dryRun = $this->option('dry-run');
57
        $show = $this->option('show');
58
        $sizeTolerance = (float) $this->option('size-tolerance');
59
60
        if ($limit && ! is_numeric($limit)) {
61
            $this->error('Limit must be a numeric value.');
62
63
            return Command::FAILURE;
64
        }
65
66
        $this->colorCLI->header('Starting rename of releases in other->misc and other-hashed categories');
67
68
        if ($dryRun) {
69
            $this->colorCLI->info('DRY RUN MODE - No changes will be made');
70
        }
71
72
        $startTime = now();
73
74
        try {
75
            // Process releases in a single pass with cascading match attempts
76
            $this->info('Processing releases with PreDB matching...');
77
            $this->processReleases($limit, $dryRun, $show, $sizeTolerance, $categorize);
0 ignored issues
show
Bug introduced by
$show of type string is incompatible with the type boolean expected by parameter $show of App\Console\Commands\Ren...ases::processReleases(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

77
            $this->processReleases($limit, $dryRun, /** @scrutinizer ignore-type */ $show, $sizeTolerance, $categorize);
Loading history...
Bug introduced by
$dryRun of type string is incompatible with the type boolean expected by parameter $dryRun of App\Console\Commands\Ren...ases::processReleases(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

77
            $this->processReleases($limit, /** @scrutinizer ignore-type */ $dryRun, $show, $sizeTolerance, $categorize);
Loading history...
78
79
            $duration = now()->diffInSeconds($startTime, true);
80
81
            $this->colorCLI->header('Processing Complete');
82
            $this->colorCLI->primary("Checked: {$this->checked} releases");
83
            $this->colorCLI->primary("Matched: {$this->matched} releases");
84
            $this->colorCLI->primary("Renamed: {$this->renamed} releases");
85
            $this->colorCLI->primary("Duration: {$duration} seconds");
86
87
            return Command::SUCCESS;
88
89
        } catch (\Exception $e) {
90
            $this->error('Error: '.$e->getMessage());
91
            $this->error($e->getTraceAsString());
92
93
            return Command::FAILURE;
94
        }
95
    }
96
97
    /**
98
     * Process releases with PreDB matching in a single pass.
99
     */
100
    protected function processReleases($limit, bool $dryRun, bool $show, float $sizeTolerance, Categorize $categorize): void
101
    {
102
        $query = Release::query()
103
            ->whereIn('categories_id', [Category::OTHER_MISC, Category::OTHER_HASHED])
104
            ->where('predb_id', 0)
105
            ->select(['id', 'guid', 'name', 'searchname', 'size', 'fromname', 'categories_id', 'groups_id'])
106
            ->orderBy('id', 'DESC'); // Process newest first
0 ignored issues
show
Bug introduced by
'id' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

106
            ->orderBy(/** @scrutinizer ignore-type */ 'id', 'DESC'); // Process newest first
Loading history...
107
108
        if ($limit) {
109
            $query->limit((int) $limit);
110
        }
111
112
        $releases = $query->get();
113
        $total = $releases->count();
114
115
        if ($total === 0) {
116
            $this->info('No releases found to process.');
117
118
            return;
119
        }
120
121
        $this->info("Processing {$total} releases...");
122
123
        foreach ($releases as $release) {
124
            $this->checked++;
125
126
            // Clean the release name for matching
127
            $cleanName = $this->cleanReleaseName($release->searchname);
128
129
            if (empty($cleanName)) {
130
                continue;
131
            }
132
133
            // Try matching in order of confidence (most strict to least strict)
134
            $matched = false;
135
136
            // 1. Title + Size Match (most reliable)
137
            if (! $matched) {
138
                $matched = $this->matchByTitleAndSize($release, $cleanName, $dryRun, $show, $sizeTolerance, $categorize);
139
            }
140
141
            // 2. Filename + Size Match
142
            if (! $matched) {
143
                $matched = $this->matchByFilenameAndSize($release, $cleanName, $dryRun, $show, $sizeTolerance, $categorize);
144
            }
145
146
            // 3. Direct Title Match (no size check)
147
            if (! $matched) {
148
                $matched = $this->matchByDirectTitle($release, $cleanName, $dryRun, $show, $categorize);
149
            }
150
151
            // 4. Direct Filename Match (no size check)
152
            if (! $matched) {
153
                $matched = $this->matchByDirectFilename($release, $cleanName, $dryRun, $show, $categorize);
154
            }
155
156
            // 5. Partial Title Match (least strict, last resort)
157
            if (! $matched) {
158
                $matched = $this->matchByPartialTitle($release, $cleanName, $dryRun, $show, $categorize);
159
            }
160
161
            if ($matched) {
162
                $this->matched++;
163
            }
164
165
            if (! $show && $this->checked % 10 === 0) {
166
                $percent = round(($this->checked / $total) * 100, 1);
167
                $this->info(
168
                    "Progress: {$percent}% ({$this->checked}/{$total}) | ".
169
                    "Matched: {$this->matched} | Renamed: {$this->renamed}"
170
                );
171
            }
172
        }
173
174
        if (! $show) {
175
            echo PHP_EOL;
176
        }
177
    }
178
179
    /**
180
     * Match release by direct title (no size check).
181
     */
182
    protected function matchByDirectTitle($release, string $cleanName, bool $dryRun, bool $show, Categorize $categorize): bool
183
    {
184
        // Try ManticoreSearch first if available
185
        if (config('nntmux.manticore.enabled') === true || config('sphinxsearch.host')) {
186
            try {
187
                $manticore = new ManticoreSearch;
188
                $results = $manticore->searchIndexes('predb_rt', $cleanName, ['title']);
189
190
                if (! empty($results['data'])) {
191
                    // Get the first matching PreDB entry from database
192
                    $predbId = $results['data'][0]['id'] ?? null;
193
                    if ($predbId) {
194
                        $predb = Predb::query()->where('id', $predbId)->first(['id', 'title', 'size', 'source']);
195
                        if ($predb && $predb->title === $cleanName) {
196
                            return $this->updateReleaseFromPredb($release, $predb, 'Direct Title Match', $dryRun, $show, $categorize);
197
                        }
198
                    }
199
                }
200
            } catch (\Exception $e) {
201
                // Fall through to direct query
202
            }
203
        }
204
205
        // Fallback to direct query
206
        $predb = Predb::query()
207
            ->where('title', $cleanName)
208
            ->first(['id', 'title', 'size', 'source']);
209
210
        if ($predb) {
211
            return $this->updateReleaseFromPredb($release, $predb, 'Direct Title Match', $dryRun, $show, $categorize);
212
        }
213
214
        return false;
215
    }
216
217
    /**
218
     * Match release by direct filename (no size check).
219
     */
220
    protected function matchByDirectFilename($release, string $cleanName, bool $dryRun, bool $show, Categorize $categorize): bool
221
    {
222
        // Try ManticoreSearch first if available
223
        if (config('nntmux.manticore.enabled') === true || config('sphinxsearch.host')) {
224
            try {
225
                $manticore = new ManticoreSearch;
226
                $results = $manticore->searchIndexes('predb_rt', $cleanName, ['filename']);
227
228
                if (! empty($results['data'])) {
229
                    // Get the first matching PreDB entry from database
230
                    $predbId = $results['data'][0]['id'] ?? null;
231
                    if ($predbId) {
232
                        $predb = Predb::query()->where('id', $predbId)->first(['id', 'title', 'size', 'source']);
233
                        if ($predb && $predb->filename === $cleanName) {
234
                            return $this->updateReleaseFromPredb($release, $predb, 'Direct Filename Match', $dryRun, $show, $categorize);
235
                        }
236
                    }
237
                }
238
            } catch (\Exception $e) {
239
                // Fall through to direct query
240
            }
241
        }
242
243
        // Fallback to direct query
244
        $predb = Predb::query()
245
            ->where('filename', $cleanName)
246
            ->first(['id', 'title', 'size', 'source']);
247
248
        if ($predb) {
249
            return $this->updateReleaseFromPredb($release, $predb, 'Direct Filename Match', $dryRun, $show, $categorize);
250
        }
251
252
        return false;
253
    }
254
255
    /**
256
     * Match release by partial title using ManticoreSearch or LIKE.
257
     */
258
    protected function matchByPartialTitle($release, string $cleanName, bool $dryRun, bool $show, Categorize $categorize): bool
259
    {
260
        // Only try partial match if clean name is reasonably long to avoid too many false positives
261
        if (strlen($cleanName) < 15) {
262
            return false;
263
        }
264
265
        // Try ManticoreSearch first if available - it's much faster for fuzzy matching
266
        if (config('nntmux.manticore.enabled') === true || config('sphinxsearch.host')) {
267
            try {
268
                $manticore = new ManticoreSearch;
269
                $results = $manticore->searchIndexes('predb_rt', $cleanName, ['title', 'filename']);
270
271
                if (! empty($results['data'])) {
272
                    // Get the best matching PreDB entry from database
273
                    foreach ($results['data'] as $result) {
274
                        $predbId = $result['id'] ?? null;
275
                        if ($predbId) {
276
                            $predb = Predb::query()->where('id', $predbId)->first(['id', 'title', 'size', 'source']);
277
                            if ($predb) {
278
                                return $this->updateReleaseFromPredb($release, $predb, 'Partial Title Match (ManticoreSearch)', $dryRun, $show, $categorize);
279
                            }
280
                        }
281
                    }
282
                }
283
            } catch (\Exception $e) {
284
                // Fall through to direct query
285
            }
286
        }
287
288
        // Fallback to LIKE query (slower)
289
        $searchPattern = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $cleanName).'%';
290
        $predb = Predb::query()
291
            ->where('title', 'LIKE', $searchPattern)
292
            ->first(['id', 'title', 'size', 'source']);
293
294
        if ($predb) {
295
            return $this->updateReleaseFromPredb($release, $predb, 'Partial Title Match', $dryRun, $show, $categorize);
296
        }
297
298
        return false;
299
    }
300
301
    /**
302
     * Process releases with exact PreDB matches (title/filename + size).
303
     *
304
     * @deprecated Use processReleases() instead for better performance
305
     */
306
    protected function processExactMatches($limit, bool $dryRun, bool $show, float $sizeTolerance, Categorize $categorize): void
307
    {
308
        $query = Release::query()
309
            ->whereIn('categories_id', [Category::OTHER_MISC, Category::OTHER_HASHED])
310
            ->where('predb_id', 0)
311
            ->select(['id', 'guid', 'name', 'searchname', 'size', 'fromname', 'categories_id', 'groups_id']);
312
313
        if ($limit) {
314
            $query->limit((int) $limit);
315
        }
316
317
        $releases = $query->get();
318
        $total = $releases->count();
319
320
        if ($total === 0) {
321
            $this->info('No releases found for exact matching.');
322
323
            return;
324
        }
325
326
        $this->info("Processing {$total} releases for exact matching...");
327
328
        foreach ($releases as $release) {
329
            $this->checked++;
330
331
            // Clean the release name for matching
332
            $cleanName = $this->cleanReleaseName($release->searchname);
333
334
            // Try to match by title and size
335
            $matched = $this->matchByTitleAndSize($release, $cleanName, $dryRun, $show, $sizeTolerance, $categorize);
336
337
            if (! $matched) {
338
                // Try to match by filename and size
339
                $matched = $this->matchByFilenameAndSize($release, $cleanName, $dryRun, $show, $sizeTolerance, $categorize);
340
            }
341
342
            if ($matched) {
343
                $this->matched++;
344
            }
345
346
            if (! $show && $this->checked % 10 === 0) {
347
                $percent = round(($this->checked / $total) * 100, 1);
348
                $this->info(
349
                    "Progress: {$percent}% ({$this->checked}/{$total}) | ".
350
                    "Matched: {$this->matched} | Renamed: {$this->renamed}"
351
                );
352
            }
353
        }
354
355
        if (! $show) {
356
            echo PHP_EOL;
357
        }
358
    }
359
360
    /**
361
     * Process releases with title matches only.
362
     */
363
    protected function processTitleMatches($limit, bool $dryRun, bool $show, Categorize $categorize): void
364
    {
365
        $query = Release::query()
366
            ->whereIn('categories_id', [Category::OTHER_MISC, Category::OTHER_HASHED])
367
            ->where('predb_id', 0)
368
            ->select(['id', 'guid', 'name', 'searchname', 'size', 'fromname', 'categories_id', 'groups_id']);
369
370
        if ($limit) {
371
            $query->limit((int) $limit);
372
        }
373
374
        $releases = $query->get();
375
        $total = $releases->count();
376
377
        if ($total === 0) {
378
            $this->info('No releases found for title matching.');
379
380
            return;
381
        }
382
383
        $this->info("Processing {$total} releases for title matching...");
384
385
        $initialChecked = $this->checked;
386
387
        foreach ($releases as $release) {
388
            $this->checked++;
389
390
            // Clean the release name for matching
391
            $cleanName = $this->cleanReleaseName($release->searchname);
392
393
            // Try fuzzy title matching
394
            $matched = $this->matchByFuzzyTitle($release, $cleanName, $dryRun, $show, $categorize);
0 ignored issues
show
Deprecated Code introduced by
The function App\Console\Commands\Ren...es::matchByFuzzyTitle() has been deprecated: Split into separate methods for better performance ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

394
            $matched = /** @scrutinizer ignore-deprecated */ $this->matchByFuzzyTitle($release, $cleanName, $dryRun, $show, $categorize);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
395
396
            if ($matched) {
397
                $this->matched++;
398
            }
399
400
            if (! $show && ($this->checked - $initialChecked) % 10 === 0) {
401
                $percent = round((($this->checked - $initialChecked) / $total) * 100, 1);
402
                $this->info(
403
                    "Progress: {$percent}% ({$this->checked}/{$total}) | ".
404
                    "Matched: {$this->matched} | Renamed: {$this->renamed}"
405
                );
406
            }
407
        }
408
409
        if (! $show) {
410
            echo PHP_EOL;
411
        }
412
    }
413
414
    /**
415
     * Match release by title and size in PreDB.
416
     */
417
    protected function matchByTitleAndSize($release, string $cleanName, bool $dryRun, bool $show, float $sizeTolerance, Categorize $categorize): bool
418
    {
419
        if (empty($cleanName)) {
420
            return false;
421
        }
422
423
        // Calculate size range for matching
424
        $sizeMin = $release->size * (1 - ($sizeTolerance / 100));
425
        $sizeMax = $release->size * (1 + ($sizeTolerance / 100));
426
427
        // Try ManticoreSearch first if available
428
        if (config('nntmux.manticore.enabled') === true || config('sphinxsearch.host')) {
429
            try {
430
                $manticore = new ManticoreSearch;
431
                $results = $manticore->searchIndexes('predb_rt', $cleanName, ['title']);
432
433
                if (! empty($results['data'])) {
434
                    // Filter results by size in PHP and get from database
435
                    foreach ($results['data'] as $result) {
436
                        $predbId = $result['id'] ?? null;
437
                        if ($predbId) {
438
                            $predb = Predb::query()->where('id', $predbId)->first(['id', 'title', 'size', 'source']);
439
                            if ($predb && $predb->title === $cleanName) {
440
                                // Check size if available
441
                                if ($predb->size === null || ($predb->size >= $sizeMin && $predb->size <= $sizeMax)) {
442
                                    return $this->updateReleaseFromPredb($release, $predb, 'Title + Size Match', $dryRun, $show, $categorize);
443
                                }
444
                            }
445
                        }
446
                    }
447
                }
448
            } catch (\Exception $e) {
449
                // Fall through to direct query
450
            }
451
        }
452
453
        // Fallback to direct query
454
        $predb = Predb::query()
455
            ->where('title', $cleanName)
456
            ->where(function ($query) use ($sizeMin, $sizeMax) {
457
                $query->whereNull('size')
458
                    ->orWhereBetween('size', [$sizeMin, $sizeMax]);
459
            })
460
            ->first(['id', 'title', 'size', 'source']);
461
462
        if ($predb) {
463
            return $this->updateReleaseFromPredb($release, $predb, 'Title + Size Match', $dryRun, $show, $categorize);
464
        }
465
466
        return false;
467
    }
468
469
    /**
470
     * Match release by filename and size in PreDB.
471
     */
472
    protected function matchByFilenameAndSize($release, string $cleanName, bool $dryRun, bool $show, float $sizeTolerance, Categorize $categorize): bool
473
    {
474
        if (empty($cleanName)) {
475
            return false;
476
        }
477
478
        // Calculate size range for matching
479
        $sizeMin = $release->size * (1 - ($sizeTolerance / 100));
480
        $sizeMax = $release->size * (1 + ($sizeTolerance / 100));
481
482
        // Try ManticoreSearch first if available
483
        if (config('nntmux.manticore.enabled') === true || config('sphinxsearch.host')) {
484
            try {
485
                $manticore = new ManticoreSearch;
486
                $results = $manticore->searchIndexes('predb_rt', $cleanName, ['filename']);
487
488
                if (! empty($results['data'])) {
489
                    // Filter results by size in PHP and get from database
490
                    foreach ($results['data'] as $result) {
491
                        $predbId = $result['id'] ?? null;
492
                        if ($predbId) {
493
                            $predb = Predb::query()->where('id', $predbId)->first(['id', 'title', 'size', 'source', 'filename']);
494
                            if ($predb && $predb->filename === $cleanName) {
495
                                // Check size if available
496
                                if ($predb->size === null || ($predb->size >= $sizeMin && $predb->size <= $sizeMax)) {
497
                                    return $this->updateReleaseFromPredb($release, $predb, 'Filename + Size Match', $dryRun, $show, $categorize);
498
                                }
499
                            }
500
                        }
501
                    }
502
                }
503
            } catch (\Exception $e) {
504
                // Fall through to direct query
505
            }
506
        }
507
508
        // Fallback to direct query
509
        $predb = Predb::query()
510
            ->where('filename', $cleanName)
511
            ->where(function ($query) use ($sizeMin, $sizeMax) {
512
                $query->whereNull('size')
513
                    ->orWhereBetween('size', [$sizeMin, $sizeMax]);
514
            })
515
            ->first(['id', 'title', 'size', 'source']);
516
517
        if ($predb) {
518
            return $this->updateReleaseFromPredb($release, $predb, 'Filename + Size Match', $dryRun, $show, $categorize);
519
        }
520
521
        return false;
522
    }
523
524
    /**
525
     * Match release by fuzzy title matching using search.
526
     *
527
     * @deprecated Split into separate methods for better performance
528
     */
529
    protected function matchByFuzzyTitle($release, string $cleanName, bool $dryRun, bool $show, Categorize $categorize): bool
530
    {
531
        // Try direct title match first
532
        if ($this->matchByDirectTitle($release, $cleanName, $dryRun, $show, $categorize)) {
533
            return true;
534
        }
535
536
        // Try filename match
537
        if ($this->matchByDirectFilename($release, $cleanName, $dryRun, $show, $categorize)) {
538
            return true;
539
        }
540
541
        // Try partial title match
542
        return $this->matchByPartialTitle($release, $cleanName, $dryRun, $show, $categorize);
543
    }
544
545
    /**
546
     * Update release from PreDB entry.
547
     */
548
    protected function updateReleaseFromPredb($release, $predb, string $matchType, bool $dryRun, bool $show, Categorize $categorize): bool
549
    {
550
        // Get old category name using cache
551
        $oldCategoryName = $this->getCategoryName($release->categories_id);
552
553
        if ($release->searchname === $predb->title) {
554
            // Names already match, just update predb_id
555
            if (! $dryRun) {
556
                Release::where('id', $release->id)->update(['predb_id' => $predb->id]);
557
            }
558
559
            if ($show) {
560
                $this->colorCLI->primary('═══════════════════════════════════════════════════════════');
0 ignored issues
show
Bug introduced by
The method primary() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

560
                $this->colorCLI->/** @scrutinizer ignore-call */ 
561
                                 primary('═══════════════════════════════════════════════════════════');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
561
                $this->colorCLI->header("Release ID: {$release->id}");
562
                $this->colorCLI->primary("GUID: {$release->guid}");
563
                $this->colorCLI->info("Match Type: {$matchType}");
564
                $this->colorCLI->info("Searchname: {$release->searchname}");
565
                $this->colorCLI->info("Category: {$oldCategoryName}");
566
                $this->colorCLI->info("PreDB Title: {$predb->title}");
567
                $this->colorCLI->info("PreDB Source: {$predb->source}");
568
                $this->colorCLI->warning('Action: Same name, only updating predb_id');
569
                if ($dryRun) {
570
                    $this->colorCLI->info('[DRY RUN - Not actually updated]');
571
                }
572
                $this->colorCLI->primary('═══════════════════════════════════════════════════════════');
573
                echo PHP_EOL;
574
            }
575
576
            return true;
577
        }
578
579
        // Names differ, perform full rename
580
        $oldName = $release->name;
0 ignored issues
show
Unused Code introduced by
The assignment to $oldName is dead and can be removed.
Loading history...
581
        $oldSearchName = $release->searchname;
582
        $newName = $predb->title;
583
        $newCategory = null;
584
        $newCategoryName = $oldCategoryName;
585
586
        if (! $dryRun) {
587
            // Update release
588
            Release::where('id', $release->id)->update([
589
                'name' => $newName,
590
                'searchname' => $newName,
591
                'isrenamed' => 1,
592
                'predb_id' => $predb->id,
593
            ]);
594
595
            // Recategorize if needed
596
            $newCategory = $categorize->determineCategory($release->groups_id, $newName);
597
            if ($newCategory !== null && is_int($newCategory) && $newCategory !== $release->categories_id) {
0 ignored issues
show
introduced by
The condition is_int($newCategory) is always false.
Loading history...
598
                Release::where('id', $release->id)->update(['categories_id' => $newCategory]);
599
                $newCategoryName = $this->getCategoryName($newCategory);
600
            }
601
602
            // Update search indexes
603
            if (config('nntmux.elasticsearch_enabled') === true) {
604
                (new ElasticSearchSiteSearch)->updateRelease($release->id);
605
            } else {
606
                (new ManticoreSearch)->updateRelease($release->id);
607
            }
608
        } else {
609
            // Dry run: calculate what the new category would be
610
            $newCategory = $categorize->determineCategory($release->groups_id, $newName);
611
            if ($newCategory !== null && is_int($newCategory) && $newCategory !== $release->categories_id) {
0 ignored issues
show
introduced by
The condition is_int($newCategory) is always false.
Loading history...
612
                $newCategoryName = $this->getCategoryName($newCategory);
613
            }
614
        }
615
616
        $this->renamed++;
617
618
        if ($show) {
619
            $this->colorCLI->primary('═══════════════════════════════════════════════════════════');
620
            $this->colorCLI->header("Release ID: {$release->id}");
621
            $this->colorCLI->primary("GUID: {$release->guid}");
622
            $this->colorCLI->info("Match Type: {$matchType}");
623
            echo PHP_EOL;
624
            $this->colorCLI->warning("OLD Searchname: {$oldSearchName}");
625
            $this->colorCLI->warning("OLD Category:   {$oldCategoryName}");
626
            echo PHP_EOL;
627
            $this->colorCLI->header("NEW Searchname: {$newName}");
628
            if ($newCategory !== null && $newCategory !== $release->categories_id) {
629
                $this->colorCLI->header("NEW Category:   {$newCategoryName}");
630
            } else {
631
                $this->colorCLI->info("NEW Category:   {$newCategoryName} (unchanged)");
632
            }
633
            echo PHP_EOL;
634
            $this->colorCLI->info("PreDB Source: {$predb->source}");
635
            if ($dryRun) {
636
                $this->colorCLI->info('[DRY RUN - Not actually updated]');
637
            }
638
            $this->colorCLI->primary('═══════════════════════════════════════════════════════════');
639
            echo PHP_EOL;
640
        }
641
642
        return true;
643
    }
644
645
    /**
646
     * Clean release name for matching.
647
     */
648
    protected function cleanReleaseName(string $name): string
649
    {
650
        // Remove common release group tags and clean up
651
        $cleaned = trim($name);
652
653
        // Remove leading/trailing dots, dashes, underscores
654
        $cleaned = trim($cleaned, '._- ');
655
656
        // Replace multiple spaces with single space
657
        $cleaned = preg_replace('/\s+/', ' ', $cleaned);
658
659
        return $cleaned;
660
    }
661
662
    /**
663
     * Get category name with caching to avoid repeated DB lookups.
664
     */
665
    protected function getCategoryName(int $categoryId): string
666
    {
667
        if (! isset($this->categoryCache[$categoryId])) {
668
            $category = Category::query()->where('id', $categoryId)->first(['title']);
669
            $this->categoryCache[$categoryId] = $category ? $category->title : "Unknown (ID: {$categoryId})";
670
        }
671
672
        return $this->categoryCache[$categoryId];
673
    }
674
}
675