Issues (868)

AdditionalProcessing/ReleaseFileManager.php (22 issues)

1
<?php
2
3
namespace App\Services\AdditionalProcessing;
4
5
use App\Models\MediaInfo as MediaInfoModel;
6
use App\Models\Predb;
7
use App\Models\Release;
8
use App\Models\ReleaseFile;
9
use App\Services\Search\ElasticSearchService;
10
use App\Services\Search\ManticoreSearchService;
11
use Blacklight\Releases;
0 ignored issues
show
The type Blacklight\Releases was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use App\Services\AdditionalProcessing\Config\ProcessingConfiguration;
0 ignored issues
show
The type App\Services\AdditionalP...ProcessingConfiguration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext;
14
use App\Services\NameFixing\NameFixingService;
15
use App\Services\NameFixing\ReleaseUpdateService;
16
use Blacklight\Nfo;
17
use Blacklight\NZB;
18
use Blacklight\ReleaseExtra;
19
use Blacklight\ReleaseImage;
20
use Illuminate\Contracts\Filesystem\FileNotFoundException;
21
use Illuminate\Support\Carbon;
22
use Illuminate\Support\Facades\File;
23
use Illuminate\Support\Facades\Log;
24
25
/**
26
 * Service for managing release-related database operations.
27
 * Handles file info, release updates, deletions, and search index updates.
28
 */
29
class ReleaseFileManager
30
{
31
    private ?ManticoreSearchService $manticore = null;
32
    private ?ElasticSearchService $elasticsearch = null;
33
34
    public function __construct(
35
        private readonly ProcessingConfiguration $config,
36
        private readonly ReleaseExtra $releaseExtra,
37
        private readonly ReleaseImage $releaseImage,
38
        private readonly Nfo $nfo,
39
        private readonly NZB $nzb,
40
        private readonly NameFixingService $nameFixingService
41
    ) {}
42
43
    /**
44
     * Add file information to the database.
45
     * @throws \Exception
46
     */
47
    public function addFileInfo(
48
        array $file,
49
        ReleaseProcessingContext $context,
50
        string $supportFileRegex
51
    ): bool {
52
        if (isset($file['error'])) {
53
            if ($this->config->debugMode) {
54
                Log::debug("Error: {$file['error']} (in: {$file['source']})");
55
            }
56
            return false;
57
        }
58
59
        if (! isset($file['name'])) {
60
            return false;
61
        }
62
63
        // Check for password
64
        if (isset($file['pass']) && $file['pass'] === true) {
65
            $context->releaseHasPassword = true;
66
            $context->passwordStatus = Releases::PASSWD_RAR;
67
            return false;
68
        }
69
70
        // Check inner file blacklist
71
        if ($this->config->innerFileBlacklist !== false
72
            && preg_match($this->config->innerFileBlacklist, $file['name'])
73
        ) {
74
            $context->releaseHasPassword = true;
75
            $context->passwordStatus = Releases::PASSWD_RAR;
76
            return false;
77
        }
78
79
        // Skip support files
80
        if (preg_match(
81
            '/(?:'.$supportFileRegex.'|part\d+|[rz]\d{1,3}|zipr\d{2,3}|\d{2,3}|zipx?|zip|rar|7z|gz|bz2|xz)(\s*\.rar)?$/i',
82
            $file['name']
83
        )) {
84
            return false;
85
        }
86
87
        // Increment total file info count
88
        $context->totalFileInfo++;
89
90
        // Don't add too many files
91
        if ($context->addedFileInfo >= 11) {
92
            return false;
93
        }
94
95
        // Check if a file already exists
96
        $exists = ReleaseFile::query()
97
            ->where([
98
                'releases_id' => $context->release->id,
99
                'name' => $file['name'],
100
                'size' => $file['size'],
101
            ])
102
            ->first();
103
104
        if ($exists !== null) {
105
            return false;
106
        }
107
108
        // Add the file
109
        $added = ReleaseFile::addReleaseFiles(
110
            $context->release->id,
111
            $file['name'],
112
            $file['size'],
113
            $file['date'],
114
            $file['pass'] ?? 0,
115
            '',
116
            $file['crc32'] ?? ''
117
        );
118
119
        if (! empty($added)) {
120
            $context->addedFileInfo++;
121
122
            // Check for codec spam
123
            if (preg_match('#(?:^|[/\\\\])Codec[/\\\\]Setup\.exe$#i', $file['name'])) {
124
                if ($this->config->debugMode) {
125
                    Log::debug('Codec spam found, setting release to potentially passworded.');
126
                }
127
                $context->releaseHasPassword = true;
128
                $context->passwordStatus = Releases::PASSWD_RAR;
129
            } elseif ($file['name'] !== '' && ! str_starts_with($file['name'], '.')) {
130
                // Run PreDB filename check
131
                $context->release['filename'] = $file['name'];
132
                $context->release['releases_id'] = $context->release->id;
133
                $this->nameFixingService->matchPreDbFiles($context->release, true, true, true);
134
            }
135
136
            return true;
137
        }
138
139
        return false;
140
    }
141
142
    /**
143
     * Update search indexes after adding file info.
144
     */
145
    public function updateSearchIndex(int $releaseId): void
146
    {
147
        if ($this->config->elasticsearchEnabled) {
148
            $this->elasticsearch()->updateRelease($releaseId);
149
        } else {
150
            $this->manticore()->updateRelease($releaseId);
151
        }
152
    }
153
154
    /**
155
     * Finalize release processing with status updates.
156
     */
157
    public function finalizeRelease(ReleaseProcessingContext $context, bool $processPasswords): void
158
    {
159
        $updateRows = ['haspreview' => 0];
160
161
        // Check for existing samples
162
        if (File::isFile($this->releaseImage->imgSavePath.$context->release->guid.'_thumb.jpg')) {
0 ignored issues
show
The property guid does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
163
            $updateRows = ['haspreview' => 1];
164
        }
165
166
        if (File::isFile($this->releaseImage->vidSavePath.$context->release->guid.'.ogv')) {
167
            $updateRows['videostatus'] = 1;
168
        }
169
170
        if (File::isFile($this->releaseImage->jpgSavePath.$context->release->guid.'_thumb.jpg')) {
171
            $updateRows['jpgstatus'] = 1;
172
        }
173
174
        // Get file count
175
        $releaseFilesCount = ReleaseFile::whereReleasesId($context->release->id)->count('releases_id') ?? 0;
176
177
        $passwordStatus = max([$context->passwordStatus]);
178
179
        // Set to no password if processing is off
180
        if (! $processPasswords) {
181
            $context->releaseHasPassword = false;
182
        }
183
184
        // Update based on conditions
185
        if (! $context->releaseHasPassword && $context->nzbHasCompressedFile && $releaseFilesCount === 0) {
186
            Release::query()->where('id', $context->release->id)->update($updateRows);
187
        } else {
188
            $updateRows['passwordstatus'] = $processPasswords ? $passwordStatus : Releases::PASSWD_NONE;
189
            $updateRows['rarinnerfilecount'] = $releaseFilesCount;
190
            Release::query()->where('id', $context->release->id)->update($updateRows);
191
        }
192
    }
193
194
    /**
195
     * Delete a broken release completely.
196
     */
197
    public function deleteRelease(Release $release): void
198
    {
199
        try {
200
            if (empty($release->id)) {
201
                return;
202
            }
203
204
            $id = (int) $release->id;
205
            $guid = $release->guid ?? '';
0 ignored issues
show
The property guid does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
206
207
            // Delete NZB file
208
            try {
209
                $nzbPath = $this->nzb->NZBPath($guid);
210
                if ($nzbPath && File::exists($nzbPath)) {
211
                    File::delete($nzbPath);
212
                }
213
            } catch (\Throwable) {
214
                // Ignore
215
            }
216
217
            // Delete preview assets
218
            try {
219
                $files = [
220
                    $this->releaseImage->imgSavePath.$guid.'_thumb.jpg',
221
                    $this->releaseImage->jpgSavePath.$guid.'_thumb.jpg',
222
                    $this->releaseImage->vidSavePath.$guid.'.ogv',
223
                ];
224
                foreach ($files as $file) {
225
                    if ($file && File::exists($file)) {
226
                        File::delete($file);
227
                    }
228
                }
229
            } catch (\Throwable) {
230
                // Ignore
231
            }
232
233
            // Delete related database rows
234
            try {
235
                ReleaseFile::where('releases_id', $id)->delete();
236
            } catch (\Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
237
            }
238
239
            try {
240
                MediaInfoModel::where('releases_id', $id)->delete();
241
            } catch (\Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
242
            }
243
244
            // Delete from search index
245
            try {
246
                if ($this->config->elasticsearchEnabled) {
247
                    $this->elasticsearch()->deleteRelease($id);
248
                } else {
249
                    $this->manticore()->deleteRelease(['i' => $id, 'g' => $guid]);
0 ignored issues
show
array('i' => $id, 'g' => $guid) of type array<string,integer|mixed|string> is incompatible with the type integer expected by parameter $id of App\Services\Search\Mant...ervice::deleteRelease(). ( Ignorable by Annotation )

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

249
                    $this->manticore()->deleteRelease(/** @scrutinizer ignore-type */ ['i' => $id, 'g' => $guid]);
Loading history...
250
                }
251
            } catch (\Throwable) {
252
                // Ignore
253
            }
254
255
            // Delete release row
256
            try {
257
                Release::where('id', $id)->delete();
258
            } catch (\Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
259
            }
260
        } catch (\Throwable) {
261
            // Last resort: swallow any exception
262
        }
263
    }
264
265
    /**
266
     * Process PAR2 file for file info and release name matching.
267
     */
268
    public function processPar2File(
269
        string $fileLocation,
270
        ReleaseProcessingContext $context,
271
        \dariusiii\rarinfo\Par2Info $par2Info
272
    ): bool {
273
        $par2Info->open($fileLocation);
274
275
        if ($par2Info->error) {
276
            return false;
277
        }
278
279
        $releaseInfo = Release::query()
280
            ->where('id', $context->release->id)
281
            ->select(['postdate', 'proc_pp'])
282
            ->first();
283
284
        if ($releaseInfo === null) {
285
            return false;
286
        }
287
288
        $postDate = Carbon::createFromFormat('Y-m-d H:i:s', $releaseInfo->postdate)->getTimestamp();
0 ignored issues
show
The property postdate does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
289
290
        // Only get new name if category is OTHER
291
        $foundName = true;
292
        if ((int) $releaseInfo->proc_pp === 0 && $this->config->renamePar2
0 ignored issues
show
The property proc_pp does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
293
            && in_array((int) $context->release->categories_id, \App\Models\Category::OTHERS_GROUP, false)
0 ignored issues
show
The property categories_id does not exist on App\Models\Release. Did you mean category_ids?
Loading history...
294
        ) {
295
            $foundName = false;
296
        }
297
298
        $filesAdded = 0;
299
300
        foreach ($par2Info->getFileList() as $file) {
301
            if (! isset($file['name'])) {
302
                continue;
303
            }
304
305
            if ($foundName && $filesAdded > 10) {
306
                break;
307
            }
308
309
            // Add to release files
310
            if ($this->config->addPAR2Files) {
311
                if ($filesAdded < 11
312
                    && ReleaseFile::query()
313
                        ->where(['releases_id' => $context->release->id, 'name' => $file['name']])
314
                        ->first() === null
315
                ) {
316
                    if (ReleaseFile::addReleaseFiles(
317
                        $context->release->id,
318
                        $file['name'],
319
                        $file['size'],
320
                        $postDate,
321
                        0,
322
                        $file['hash_16K']
323
                    )) {
324
                        $filesAdded++;
325
                    }
326
                }
327
            } else {
328
                $filesAdded++;
329
            }
330
331
            // Try to get a new name
332
            if (! $foundName) {
333
                $context->release->textstring = $file['name'];
0 ignored issues
show
The property textstring does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
334
                $context->release->releases_id = $context->release->id;
0 ignored issues
show
The property releases_id does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
335
                if ($this->nameFixingService->checkName($context->release, $this->config->echoCLI, 'PAR2, ', true, true)) {
336
                    $foundName = true;
337
                }
338
            }
339
        }
340
341
        // Update file count
342
        Release::query()->where('id', $context->release->id)->increment('rarinnerfilecount', $filesAdded);
343
        $context->foundPAR2Info = true;
344
345
        return true;
346
    }
347
348
    /**
349
     * Process NFO file with enhanced detection capabilities.
350
     *
351
     * Supports multiple NFO naming conventions:
352
     * - Standard: .nfo, .diz, .info
353
     * - Alternative: file_id.diz, readme.txt, info.txt
354
     * - Scene-style: 00-groupname.nfo, groupname-releasename.nfo
355
     */
356
    public function processNfoFile(
357
        string $fileLocation,
358
        ReleaseProcessingContext $context,
359
        \Blacklight\NNTP $nntp
360
    ): bool {
361
        try {
362
            $data = File::get($fileLocation);
363
364
            // Try to detect and convert encoding
365
            $data = $this->normalizeNfoEncoding($data);
366
367
            if ($this->nfo->isNFO($data, $context->release->guid)
0 ignored issues
show
The property guid does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
368
                && $this->nfo->addAlternateNfo($data, $context->release, $nntp)
369
            ) {
370
                $context->releaseHasNoNFO = false;
371
                return true;
372
            }
373
        } catch (FileNotFoundException $e) {
374
            Log::warning("Could not read potential NFO file: {$fileLocation} - {$e->getMessage()}");
375
        }
376
377
        return false;
378
    }
379
380
    /**
381
     * Check if a filename looks like an NFO file.
382
     *
383
     * @param string $filename The filename to check.
384
     * @return bool True if the filename matches NFO patterns.
385
     */
386
    public function isNfoFilename(string $filename): bool
387
    {
388
        // Standard NFO extensions
389
        if (preg_match('/\.(?:nfo|diz|info?)$/i', $filename)) {
390
            return true;
391
        }
392
393
        // Alternative NFO filenames
394
        $nfoPatterns = [
395
            '/^(?:file[_-]?id|readme|release|info(?:rmation)?|about|notes?)\.(?:txt|diz)$/i',
396
            '/^00-[a-z0-9_-]+\.nfo$/i',           // Scene: 00-group.nfo
397
            '/^0+-[a-z0-9_-]+\.nfo$/i',           // Scene variations
398
            '/^[a-z0-9_-]+-[a-z0-9_.-]+\.nfo$/i', // Scene: group-release.nfo
399
            '/info\.txt$/i',                      // info.txt (common alternative)
400
        ];
401
402
        $basename = basename($filename);
403
        foreach ($nfoPatterns as $pattern) {
404
            if (preg_match($pattern, $basename)) {
405
                return true;
406
            }
407
        }
408
409
        return false;
410
    }
411
412
    /**
413
     * Normalize NFO encoding to UTF-8.
414
     *
415
     * NFO files often use CP437 (DOS) encoding for ASCII art.
416
     * This method attempts to detect and convert various encodings.
417
     *
418
     * @param string $data Raw NFO data.
419
     * @return string UTF-8 encoded NFO data.
420
     */
421
    protected function normalizeNfoEncoding(string $data): string
422
    {
423
        // Check for UTF-8 BOM and remove it
424
        if (str_starts_with($data, "\xEF\xBB\xBF")) {
425
            $data = substr($data, 3);
426
        }
427
428
        // Check for UTF-16 BOM
429
        if (str_starts_with($data, "\xFF\xFE")) {
430
            // UTF-16 LE
431
            $data = mb_convert_encoding(substr($data, 2), 'UTF-8', 'UTF-16LE');
432
        } elseif (str_starts_with($data, "\xFE\xFF")) {
433
            // UTF-16 BE
434
            $data = mb_convert_encoding(substr($data, 2), 'UTF-8', 'UTF-16BE');
435
        }
436
437
        // If already valid UTF-8, return as-is
438
        if (mb_check_encoding($data, 'UTF-8')) {
439
            return $data;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $data could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
440
        }
441
442
        // Try CP437 (DOS encoding - common for scene NFOs with ASCII art)
443
        // Use the cp437toUTF helper function
444
        if (function_exists('cp437toUTF')) {
445
            return cp437toUTF($data);
0 ignored issues
show
It seems like $data can also be of type array; however, parameter $string of cp437toUTF() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

445
            return cp437toUTF(/** @scrutinizer ignore-type */ $data);
Loading history...
446
        }
447
448
        // Fallback: try ISO-8859-1 (Latin-1)
449
        $converted = @mb_convert_encoding($data, 'UTF-8', 'ISO-8859-1');
450
        if ($converted !== false) {
451
            return $converted;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $converted could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
452
        }
453
454
        // Last resort: force UTF-8 with error handling
455
        return mb_convert_encoding($data, 'UTF-8', 'UTF-8');
0 ignored issues
show
Bug Best Practice introduced by
The expression return mb_convert_encodi...data, 'UTF-8', 'UTF-8') could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
456
    }
457
458
    /**
459
     * Handle release name extraction from RAR file content.
460
     */
461
    public function processReleaseNameFromRar(
462
        array $dataSummary,
463
        ReleaseProcessingContext $context
464
    ): void {
465
        $fileData = $dataSummary['file_list'] ?? [];
466
        if (empty($fileData)) {
467
            return;
468
        }
469
470
        $rarFileName = array_column($fileData, 'name');
471
        if (empty($rarFileName[0])) {
472
            return;
473
        }
474
475
        $extractedName = $this->extractReleaseNameFromFile($rarFileName[0]);
476
477
        if ($extractedName !== null) {
478
            $preCheck = Predb::whereTitle($extractedName)->first();
479
            $context->release->preid = $preCheck !== null ? $preCheck->value('id') : 0;
0 ignored issues
show
The property preid does not exist on App\Models\Release. Did you mean predb?
Loading history...
480
            $candidate = $preCheck->title ?? $extractedName;
481
            $candidate = $this->normalizeCandidateTitle($candidate);
482
483
            if ($this->isPlausibleReleaseTitle($candidate)) {
484
                (new ReleaseUpdateService())->updateRelease(
485
                    $context->release,
486
                    $candidate,
487
                    'RarInfo FileName Match',
488
                    true,
489
                    'Filenames, ',
490
                    true,
491
                    true,
492
                    $context->release->preid
0 ignored issues
show
It seems like $context->release->preid can also be of type App\Models\Predb and Illuminate\Database\Eloq...gHasThroughRelationship; however, parameter $preId of App\Services\NameFixing\...ervice::updateRelease() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

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

492
                    /** @scrutinizer ignore-type */ $context->release->preid
Loading history...
493
                );
494
            } elseif ($this->config->debugMode) {
495
                Log::debug('RarInfo: Ignored low-quality candidate "'.$candidate.'" from inner file name.');
496
            }
497
        } elseif (! empty($dataSummary['archives'][$rarFileName[0]]['file_list'])) {
498
            // Try nested archive
499
            $archiveData = $dataSummary['archives'][$rarFileName[0]]['file_list'];
500
            $archiveFileName = array_column($archiveData, 'name');
501
            $extractedName = $this->extractReleaseNameFromFile($archiveFileName[0] ?? '');
502
503
            if ($extractedName !== null) {
504
                $preCheck = Predb::whereTitle($extractedName)->first();
505
                $context->release->preid = $preCheck !== null ? $preCheck->value('id') : 0;
506
                $candidate = $preCheck->title ?? $extractedName;
507
                $candidate = $this->normalizeCandidateTitle($candidate);
508
509
                if ($this->isPlausibleReleaseTitle($candidate)) {
510
                    (new ReleaseUpdateService())->updateRelease(
511
                        $context->release,
512
                        $candidate,
513
                        'RarInfo FileName Match',
514
                        true,
515
                        'Filenames, ',
516
                        true,
517
                        true,
518
                        $context->release->preid
519
                    );
520
                }
521
            }
522
        }
523
    }
524
525
    /**
526
     * Extract the release name from a filename.
527
     */
528
    private function extractReleaseNameFromFile(string $filename): ?string
529
    {
530
        $basename = basename($filename);
531
        $cleaned = preg_replace(
532
            '/\.(mkv|avi|mp4|m4v|mpg|mpeg|wmv|flv|mov|ts|vob|iso|divx|par2?|nfo|sfv|nzb|rar|zip|r\d{2,3}|pkg|exe|msi)$/i',
533
            '',
534
            $basename
535
        );
536
537
        if (preg_match('/^(.+[-.][A-Za-z0-9_]{2,})$/i', $cleaned, $match)) {
538
            return ucwords($match[1], '.-_ ');
539
        }
540
541
        if (preg_match(ReleaseUpdateService::PREDB_REGEX, $cleaned, $hit)) {
542
            return ucwords($hit[0], '.');
543
        }
544
545
        return null;
546
    }
547
548
    /**
549
     * Normalize a candidate title.
550
     */
551
    private function normalizeCandidateTitle(string $title): string
552
    {
553
        $t = trim($title);
554
        $t = preg_replace('/\.(mkv|avi|mp4|m4v|mpg|mpeg|wmv|flv|mov|ts|vob|iso|divx)$/i', '', $t) ?? $t;
555
        $t = preg_replace('/\.(par2?|nfo|sfv|nzb|rar|zip|r\d{2,3}|pkg|exe|msi|jpe?g|png|gif|bmp)$/i', '', $t) ?? $t;
556
        $t = preg_replace('/[.\-_ ](?:part|vol|r)\d+(?:\+\d+)?$/i', '', $t) ?? $t;
557
        $t = preg_replace('/[\s_]+/', ' ', $t) ?? $t;
558
        return trim($t, " .-_\t\r\n");
559
    }
560
561
    /**
562
     * Check if a title is plausible for release naming.
563
     */
564
    private function isPlausibleReleaseTitle(string $title): bool
565
    {
566
        $t = trim($title);
567
        if ($t === '' || strlen($t) < 12) {
568
            return false;
569
        }
570
571
        $wordCount = preg_match_all('/[A-Za-z0-9]{3,}/', $t);
572
        if ($wordCount < 2) {
573
            return false;
574
        }
575
576
        if (preg_match('/(?:^|[.\-_ ])(?:part|vol|r)\d+(?:\+\d+)?$/i', $t)) {
577
            return false;
578
        }
579
580
        if (preg_match('/^(setup|install|installer|patch|update|crack|keygen)\d*[\s._-]/i', $t)) {
581
            return false;
582
        }
583
584
        $hasGroupSuffix = (bool) preg_match('/[-.][A-Za-z0-9]{2,}$/', $t);
585
        $hasYear = (bool) preg_match('/\b(19|20)\d{2}\b/', $t);
586
        $hasQuality = (bool) preg_match('/\b(480p|720p|1080p|2160p|4k|webrip|web[ .-]?dl|bluray|bdrip|dvdrip|hdtv|hdrip|xvid|x264|x265|hevc|h\.?264|ts|cam|r5|proper|repack)\b/i', $t);
587
        $hasTV = (bool) preg_match('/\bS\d{1,2}[Eex]\d{1,3}\b/i', $t);
588
        $hasXXX = (bool) preg_match('/\bXXX\b/i', $t);
589
590
        return $hasGroupSuffix || ($hasTV && $hasQuality) || ($hasYear && ($hasQuality || $hasTV)) || $hasXXX;
591
    }
592
593
    private function manticore(): ManticoreSearchService
594
    {
595
        if ($this->manticore === null) {
596
            $this->manticore = app(ManticoreSearchService::class);
597
        }
598
        return $this->manticore;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->manticore could return the type null which is incompatible with the type-hinted return App\Services\Search\ManticoreSearchService. Consider adding an additional type-check to rule them out.
Loading history...
599
    }
600
601
    private function elasticsearch(): ElasticSearchService
602
    {
603
        if ($this->elasticsearch === null) {
604
            $this->elasticsearch = app(ElasticSearchService::class);
605
        }
606
        return $this->elasticsearch;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->elasticsearch could return the type null which is incompatible with the type-hinted return App\Services\Search\ElasticSearchService. Consider adding an additional type-check to rule them out.
Loading history...
607
    }
608
}
609
610