Passed
Push — master ( ae0ea4...59225e )
by Darko
10:57
created

RenameOtherMiscReleases::processReleases()   F

Complexity

Conditions 14
Paths 522

Size

Total Lines 76
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 38
c 1
b 0
f 0
dl 0
loc 76
rs 2.7637
cc 14
nc 522
nop 5

How to fix   Long Method    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
        $predb = Predb::query()
185
            ->where('title', $cleanName)
186
            ->first(['id', 'title', 'size', 'source']);
187
188
        if ($predb) {
189
            return $this->updateReleaseFromPredb($release, $predb, 'Direct Title Match', $dryRun, $show, $categorize);
190
        }
191
192
        return false;
193
    }
194
195
    /**
196
     * Match release by direct filename (no size check).
197
     */
198
    protected function matchByDirectFilename($release, string $cleanName, bool $dryRun, bool $show, Categorize $categorize): bool
199
    {
200
        $predb = Predb::query()
201
            ->where('filename', $cleanName)
202
            ->first(['id', 'title', 'size', 'source']);
203
204
        if ($predb) {
205
            return $this->updateReleaseFromPredb($release, $predb, 'Direct Filename Match', $dryRun, $show, $categorize);
206
        }
207
208
        return false;
209
    }
210
211
    /**
212
     * Match release by partial title using LIKE.
213
     */
214
    protected function matchByPartialTitle($release, string $cleanName, bool $dryRun, bool $show, Categorize $categorize): bool
215
    {
216
        // Only try partial match if clean name is reasonably long to avoid too many false positives
217
        if (strlen($cleanName) < 15) {
218
            return false;
219
        }
220
221
        $searchPattern = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $cleanName).'%';
222
        $predb = Predb::query()
223
            ->where('title', 'LIKE', $searchPattern)
224
            ->first(['id', 'title', 'size', 'source']);
225
226
        if ($predb) {
227
            return $this->updateReleaseFromPredb($release, $predb, 'Partial Title Match', $dryRun, $show, $categorize);
228
        }
229
230
        return false;
231
    }
232
233
    /**
234
     * Process releases with exact PreDB matches (title/filename + size).
235
     *
236
     * @deprecated Use processReleases() instead for better performance
237
     */
238
    protected function processExactMatches($limit, bool $dryRun, bool $show, float $sizeTolerance, Categorize $categorize): void
239
    {
240
        $query = Release::query()
241
            ->whereIn('categories_id', [Category::OTHER_MISC, Category::OTHER_HASHED])
242
            ->where('predb_id', 0)
243
            ->select(['id', 'guid', 'name', 'searchname', 'size', 'fromname', 'categories_id', 'groups_id']);
244
245
        if ($limit) {
246
            $query->limit((int) $limit);
247
        }
248
249
        $releases = $query->get();
250
        $total = $releases->count();
251
252
        if ($total === 0) {
253
            $this->info('No releases found for exact matching.');
254
255
            return;
256
        }
257
258
        $this->info("Processing {$total} releases for exact matching...");
259
260
        foreach ($releases as $release) {
261
            $this->checked++;
262
263
            // Clean the release name for matching
264
            $cleanName = $this->cleanReleaseName($release->searchname);
265
266
            // Try to match by title and size
267
            $matched = $this->matchByTitleAndSize($release, $cleanName, $dryRun, $show, $sizeTolerance, $categorize);
268
269
            if (! $matched) {
270
                // Try to match by filename and size
271
                $matched = $this->matchByFilenameAndSize($release, $cleanName, $dryRun, $show, $sizeTolerance, $categorize);
272
            }
273
274
            if ($matched) {
275
                $this->matched++;
276
            }
277
278
            if (! $show && $this->checked % 10 === 0) {
279
                $percent = round(($this->checked / $total) * 100, 1);
280
                $this->info(
281
                    "Progress: {$percent}% ({$this->checked}/{$total}) | ".
282
                    "Matched: {$this->matched} | Renamed: {$this->renamed}"
283
                );
284
            }
285
        }
286
287
        if (! $show) {
288
            echo PHP_EOL;
289
        }
290
    }
291
292
    /**
293
     * Process releases with title matches only.
294
     */
295
    protected function processTitleMatches($limit, bool $dryRun, bool $show, Categorize $categorize): void
296
    {
297
        $query = Release::query()
298
            ->whereIn('categories_id', [Category::OTHER_MISC, Category::OTHER_HASHED])
299
            ->where('predb_id', 0)
300
            ->select(['id', 'guid', 'name', 'searchname', 'size', 'fromname', 'categories_id', 'groups_id']);
301
302
        if ($limit) {
303
            $query->limit((int) $limit);
304
        }
305
306
        $releases = $query->get();
307
        $total = $releases->count();
308
309
        if ($total === 0) {
310
            $this->info('No releases found for title matching.');
311
312
            return;
313
        }
314
315
        $this->info("Processing {$total} releases for title matching...");
316
317
        $initialChecked = $this->checked;
318
319
        foreach ($releases as $release) {
320
            $this->checked++;
321
322
            // Clean the release name for matching
323
            $cleanName = $this->cleanReleaseName($release->searchname);
324
325
            // Try fuzzy title matching
326
            $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

326
            $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...
327
328
            if ($matched) {
329
                $this->matched++;
330
            }
331
332
            if (! $show && ($this->checked - $initialChecked) % 10 === 0) {
333
                $percent = round((($this->checked - $initialChecked) / $total) * 100, 1);
334
                $this->info(
335
                    "Progress: {$percent}% ({$this->checked}/{$total}) | ".
336
                    "Matched: {$this->matched} | Renamed: {$this->renamed}"
337
                );
338
            }
339
        }
340
341
        if (! $show) {
342
            echo PHP_EOL;
343
        }
344
    }
345
346
    /**
347
     * Match release by title and size in PreDB.
348
     */
349
    protected function matchByTitleAndSize($release, string $cleanName, bool $dryRun, bool $show, float $sizeTolerance, Categorize $categorize): bool
350
    {
351
        if (empty($cleanName)) {
352
            return false;
353
        }
354
355
        // Calculate size range for matching
356
        $sizeMin = $release->size * (1 - ($sizeTolerance / 100));
357
        $sizeMax = $release->size * (1 + ($sizeTolerance / 100));
358
359
        $predb = Predb::query()
360
            ->where('title', $cleanName)
361
            ->where(function ($query) use ($sizeMin, $sizeMax) {
362
                $query->whereNull('size')
363
                    ->orWhereBetween('size', [$sizeMin, $sizeMax]);
364
            })
365
            ->first(['id', 'title', 'size', 'source']);
366
367
        if ($predb) {
368
            return $this->updateReleaseFromPredb($release, $predb, 'Title + Size Match', $dryRun, $show, $categorize);
369
        }
370
371
        return false;
372
    }
373
374
    /**
375
     * Match release by filename and size in PreDB.
376
     */
377
    protected function matchByFilenameAndSize($release, string $cleanName, bool $dryRun, bool $show, float $sizeTolerance, Categorize $categorize): bool
378
    {
379
        if (empty($cleanName)) {
380
            return false;
381
        }
382
383
        // Calculate size range for matching
384
        $sizeMin = $release->size * (1 - ($sizeTolerance / 100));
385
        $sizeMax = $release->size * (1 + ($sizeTolerance / 100));
386
387
        $predb = Predb::query()
388
            ->where('filename', $cleanName)
389
            ->where(function ($query) use ($sizeMin, $sizeMax) {
390
                $query->whereNull('size')
391
                    ->orWhereBetween('size', [$sizeMin, $sizeMax]);
392
            })
393
            ->first(['id', 'title', 'size', 'source']);
394
395
        if ($predb) {
396
            return $this->updateReleaseFromPredb($release, $predb, 'Filename + Size Match', $dryRun, $show, $categorize);
397
        }
398
399
        return false;
400
    }
401
402
    /**
403
     * Match release by fuzzy title matching using search.
404
     *
405
     * @deprecated Split into separate methods for better performance
406
     */
407
    protected function matchByFuzzyTitle($release, string $cleanName, bool $dryRun, bool $show, Categorize $categorize): bool
408
    {
409
        // Try direct title match first
410
        if ($this->matchByDirectTitle($release, $cleanName, $dryRun, $show, $categorize)) {
411
            return true;
412
        }
413
414
        // Try filename match
415
        if ($this->matchByDirectFilename($release, $cleanName, $dryRun, $show, $categorize)) {
416
            return true;
417
        }
418
419
        // Try partial title match
420
        return $this->matchByPartialTitle($release, $cleanName, $dryRun, $show, $categorize);
421
    }
422
423
    /**
424
     * Update release from PreDB entry.
425
     */
426
    protected function updateReleaseFromPredb($release, $predb, string $matchType, bool $dryRun, bool $show, Categorize $categorize): bool
427
    {
428
        // Get old category name using cache
429
        $oldCategoryName = $this->getCategoryName($release->categories_id);
430
431
        if ($release->searchname === $predb->title) {
432
            // Names already match, just update predb_id
433
            if (! $dryRun) {
434
                Release::where('id', $release->id)->update(['predb_id' => $predb->id]);
435
            }
436
437
            if ($show) {
438
                $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

438
                $this->colorCLI->/** @scrutinizer ignore-call */ 
439
                                 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...
439
                $this->colorCLI->header("Release ID: {$release->id}");
440
                $this->colorCLI->primary("GUID: {$release->guid}");
441
                $this->colorCLI->info("Match Type: {$matchType}");
442
                $this->colorCLI->info("Searchname: {$release->searchname}");
443
                $this->colorCLI->info("Category: {$oldCategoryName}");
444
                $this->colorCLI->info("PreDB Title: {$predb->title}");
445
                $this->colorCLI->info("PreDB Source: {$predb->source}");
446
                $this->colorCLI->warning('Action: Same name, only updating predb_id');
447
                if ($dryRun) {
448
                    $this->colorCLI->info('[DRY RUN - Not actually updated]');
449
                }
450
                $this->colorCLI->primary('═══════════════════════════════════════════════════════════');
451
                echo PHP_EOL;
452
            }
453
454
            return true;
455
        }
456
457
        // Names differ, perform full rename
458
        $oldName = $release->name;
0 ignored issues
show
Unused Code introduced by
The assignment to $oldName is dead and can be removed.
Loading history...
459
        $oldSearchName = $release->searchname;
460
        $newName = $predb->title;
461
        $newCategory = null;
462
        $newCategoryName = $oldCategoryName;
463
464
        if (! $dryRun) {
465
            // Update release
466
            Release::where('id', $release->id)->update([
467
                'name' => $newName,
468
                'searchname' => $newName,
469
                'isrenamed' => 1,
470
                'predb_id' => $predb->id,
471
            ]);
472
473
            // Recategorize if needed
474
            $newCategory = $categorize->determineCategory($release->groups_id, $newName);
475
            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...
476
                Release::where('id', $release->id)->update(['categories_id' => $newCategory]);
477
                $newCategoryName = $this->getCategoryName($newCategory);
478
            }
479
480
            // Update search indexes
481
            if (config('nntmux.elasticsearch_enabled') === true) {
482
                (new ElasticSearchSiteSearch)->updateRelease($release->id);
483
            } else {
484
                (new ManticoreSearch)->updateRelease($release->id);
485
            }
486
        } else {
487
            // Dry run: calculate what the new category would be
488
            $newCategory = $categorize->determineCategory($release->groups_id, $newName);
489
            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...
490
                $newCategoryName = $this->getCategoryName($newCategory);
491
            }
492
        }
493
494
        $this->renamed++;
495
496
        if ($show) {
497
            $this->colorCLI->primary('═══════════════════════════════════════════════════════════');
498
            $this->colorCLI->header("Release ID: {$release->id}");
499
            $this->colorCLI->primary("GUID: {$release->guid}");
500
            $this->colorCLI->info("Match Type: {$matchType}");
501
            echo PHP_EOL;
502
            $this->colorCLI->warning("OLD Searchname: {$oldSearchName}");
503
            $this->colorCLI->warning("OLD Category:   {$oldCategoryName}");
504
            echo PHP_EOL;
505
            $this->colorCLI->header("NEW Searchname: {$newName}");
506
            if ($newCategory !== null && $newCategory !== $release->categories_id) {
507
                $this->colorCLI->header("NEW Category:   {$newCategoryName}");
508
            } else {
509
                $this->colorCLI->info("NEW Category:   {$newCategoryName} (unchanged)");
510
            }
511
            echo PHP_EOL;
512
            $this->colorCLI->info("PreDB Source: {$predb->source}");
513
            if ($dryRun) {
514
                $this->colorCLI->info('[DRY RUN - Not actually updated]');
515
            }
516
            $this->colorCLI->primary('═══════════════════════════════════════════════════════════');
517
            echo PHP_EOL;
518
        }
519
520
        return true;
521
    }
522
523
    /**
524
     * Clean release name for matching.
525
     */
526
    protected function cleanReleaseName(string $name): string
527
    {
528
        // Remove common release group tags and clean up
529
        $cleaned = trim($name);
530
531
        // Remove leading/trailing dots, dashes, underscores
532
        $cleaned = trim($cleaned, '._- ');
533
534
        // Replace multiple spaces with single space
535
        $cleaned = preg_replace('/\s+/', ' ', $cleaned);
536
537
        return $cleaned;
538
    }
539
540
    /**
541
     * Get category name with caching to avoid repeated DB lookups.
542
     */
543
    protected function getCategoryName(int $categoryId): string
544
    {
545
        if (! isset($this->categoryCache[$categoryId])) {
546
            $category = Category::query()->where('id', $categoryId)->first(['title']);
547
            $this->categoryCache[$categoryId] = $category ? $category->title : "Unknown (ID: {$categoryId})";
548
        }
549
550
        return $this->categoryCache[$categoryId];
551
    }
552
}
553