AdultProcessingPipeline   F
last analyzed

Complexity

Total Complexity 105

Size/Duplication

Total Lines 741
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 105
eloc 359
c 1
b 0
f 0
dl 0
loc 741
rs 2

22 Methods

Rating   Name   Duplication   Size   Complexity  
A canUseAsync() 0 5 3
F updateXXXInfo() 0 121 27
A getReleasesToProcess() 0 18 1
B processXXXReleases() 0 48 9
A getPipes() 0 3 1
A updateReleaseXxxId() 0 3 1
A createForChildProcess() 0 7 1
B processReleaseItem() 0 71 11
A processMovie() 0 17 2
A insertGenre() 0 11 2
B parseXXXSearchName() 0 27 8
A processReleaseInChildProcess() 0 49 5
A getStats() 0 3 1
A processRelease() 0 17 2
A getDefaultPipes() 0 10 1
D processBatch() 0 81 15
A outputStats() 0 13 3
A checkXXXInfoExists() 0 5 2
A addPipe() 0 6 1
A resetStats() 0 9 1
A __construct() 0 24 3
A getGenreID() 0 23 5

How to fix   Complexity   

Complex Class

Complex classes like AdultProcessingPipeline often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AdultProcessingPipeline, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Services\AdultProcessing;
4
5
use App\Models\Category;
6
use App\Models\Genre;
7
use App\Models\Release;
8
use App\Models\Settings;
9
use App\Models\XxxInfo;
10
use App\Services\AdultProcessing\Pipes\AbstractAdultProviderPipe;
11
use App\Services\AdultProcessing\Pipes\AdePipe;
12
use App\Services\AdultProcessing\Pipes\AdmPipe;
13
use App\Services\AdultProcessing\Pipes\AebnPipe;
14
use App\Services\AdultProcessing\Pipes\Data18Pipe;
15
use App\Services\AdultProcessing\Pipes\HotmoviesPipe;
16
use App\Services\AdultProcessing\Pipes\IafdPipe;
17
use App\Services\AdultProcessing\Pipes\PoppornPipe;
18
use App\Services\ReleaseImageService;
19
use Illuminate\Pipeline\Pipeline;
20
use Illuminate\Support\Collection;
21
use Illuminate\Support\Facades\Concurrency;
22
use Illuminate\Support\Facades\Log;
23
24
/**
25
 * Pipeline-based adult movie processing service using Laravel Pipeline.
26
 *
27
 * This service uses Laravel's Pipeline to orchestrate multiple adult movie data providers
28
 * to process releases and match them against movie metadata from various sources.
29
 *
30
 * It also supports parallel processing of multiple releases using Laravel's native Concurrency facade.
31
 */
32
class AdultProcessingPipeline
33
{
34
    /**
35
     * @var Collection<AbstractAdultProviderPipe>
36
     */
37
    protected Collection $pipes;
38
39
    protected int $movieQty;
40
41
    protected bool $echoOutput;
42
43
    protected ReleaseImageService $releaseImage;
44
45
    protected string $imgSavePath;
46
47
    protected string $cookie;
48
49
    protected string $showPasswords;
50
51
    /**
52
     * Processing statistics.
53
     */
54
    protected array $stats = [
55
        'processed' => 0,
56
        'matched' => 0,
57
        'failed' => 0,
58
        'skipped' => 0,
59
        'duration' => 0.0,
60
        'providers' => [],
61
    ];
62
63
    /**
64
     * @param  iterable<AbstractAdultProviderPipe>  $pipes
65
     */
66
    public function __construct(iterable $pipes = [], bool $echoOutput = true)
67
    {
68
        $this->pipes = collect($pipes);
0 ignored issues
show
Bug introduced by
It seems like $pipes can also be of type array; however, parameter $value of collect() does only seem to accept Illuminate\Contracts\Support\Arrayable, 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

68
        $this->pipes = collect(/** @scrutinizer ignore-type */ $pipes);
Loading history...
69
70
        if ($this->pipes->isEmpty()) {
71
            $this->pipes = $this->getDefaultPipes();
72
        }
73
74
        $this->pipes = $this->pipes->sortBy(fn (AbstractAdultProviderPipe $p) => $p->getPriority());
75
76
        // Try to get settings from database, but handle failures gracefully (e.g., in child processes)
77
        try {
78
            $this->movieQty = (int) (Settings::settingValue('maxxxxprocessed') ?? 100);
79
            $this->showPasswords = app(\App\Services\Releases\ReleaseBrowseService::class)->showPasswords();
80
        } catch (\Exception $e) {
81
            // Fallback values for child processes where DB might not be available
82
            $this->movieQty = 100;
83
            $this->showPasswords = '';
84
        }
85
86
        $this->echoOutput = $echoOutput;
87
        $this->releaseImage = new ReleaseImageService;
88
        $this->imgSavePath = storage_path('covers/xxx/');
89
        $this->cookie = resource_path('tmp/xxx.cookie');
90
    }
91
92
    /**
93
     * Create a lightweight instance for child process use.
94
     * This avoids database calls that may fail in forked processes.
95
     */
96
    protected static function createForChildProcess(string $cookie, bool $echoOutput, string $imgSavePath): self
97
    {
98
        $instance = new self([], $echoOutput);
99
        $instance->cookie = $cookie;
100
        $instance->imgSavePath = $imgSavePath;
101
102
        return $instance;
103
    }
104
105
    /**
106
     * Get the default pipes in priority order.
107
     */
108
    protected function getDefaultPipes(): Collection
109
    {
110
        return collect([
111
            new AebnPipe,
112
            new IafdPipe,
113
            new Data18Pipe,
114
            new PoppornPipe,
115
            new AdmPipe,
116
            new AdePipe,
117
            new HotmoviesPipe,
118
        ]);
119
    }
120
121
    /**
122
     * Add a provider pipe to the pipeline.
123
     */
124
    public function addPipe(AbstractAdultProviderPipe $pipe): self
125
    {
126
        $this->pipes->push($pipe);
127
        $this->pipes = $this->pipes->sortBy(fn (AbstractAdultProviderPipe $p) => $p->getPriority());
128
129
        return $this;
130
    }
131
132
    /**
133
     * Process a single movie title through the pipeline.
134
     *
135
     * @param  string  $movie  Movie title to search for
136
     * @param  bool  $debug  Whether to include debug information
137
     * @return array Processing result
138
     */
139
    public function processMovie(string $movie, bool $debug = false): array
140
    {
141
        $context = AdultReleaseContext::fromTitle($movie);
142
        $passable = new AdultProcessingPassable($context, $debug, $this->cookie);
143
144
        // Set echo output on all pipes
145
        foreach ($this->pipes as $pipe) {
146
            $pipe->setEchoOutput($this->echoOutput);
147
        }
148
149
        /** @var AdultProcessingPassable $result */
150
        $result = app(Pipeline::class)
151
            ->send($passable)
152
            ->through($this->pipes->values()->all())
153
            ->thenReturn();
154
155
        return $result->toArray();
156
    }
157
158
    /**
159
     * Process a single release through the pipeline.
160
     *
161
     * @param  array|object  $release  Release data
162
     * @param  string  $cleanTitle  Cleaned movie title
163
     * @param  bool  $debug  Whether to include debug information
164
     * @return array Processing result
165
     */
166
    public function processRelease(array|object $release, string $cleanTitle, bool $debug = false): array
167
    {
168
        $context = AdultReleaseContext::fromRelease($release, $cleanTitle);
169
        $passable = new AdultProcessingPassable($context, $debug, $this->cookie);
170
171
        // Set echo output on all pipes
172
        foreach ($this->pipes as $pipe) {
173
            $pipe->setEchoOutput($this->echoOutput);
174
        }
175
176
        /** @var AdultProcessingPassable $result */
177
        $result = app(Pipeline::class)
178
            ->send($passable)
179
            ->through($this->pipes->values()->all())
180
            ->thenReturn();
181
182
        return $result->toArray();
183
    }
184
185
    /**
186
     * Process all XXX releases where xxxinfo_id is 0.
187
     *
188
     * Uses Laravel's native Concurrency facade for parallel processing when possible.
189
     */
190
    public function processXXXReleases(): void
191
    {
192
        $startTime = microtime(true);
193
        $this->resetStats();
194
195
        try {
196
            $releases = $this->getReleasesToProcess();
197
            $releaseCount = count($releases);
198
199
            if ($releaseCount === 0) {
200
                if ($this->echoOutput) {
201
                    cli()->header('No XXX releases to process.');
202
                }
203
204
                return;
205
            }
206
207
            if ($this->echoOutput) {
208
                cli()->header('Processing '.$releaseCount.' XXX releases using pipeline.');
209
            }
210
211
            // Process releases in batches for parallel processing
212
            $batchSize = min(5, $releaseCount); // Process up to 5 at a time
213
            $batches = array_chunk($releases->toArray(), $batchSize);
214
215
            foreach ($batches as $batchIndex => $batch) {
216
                if ($this->echoOutput) {
217
                    cli()->info('Processing batch '.($batchIndex + 1).' of '.count($batches));
218
                }
219
                $this->processBatch($batch);
220
            }
221
        } catch (\Throwable $e) {
222
            Log::error('processXXXReleases failed: '.$e->getMessage(), [
223
                'exception' => get_class($e),
224
                'file' => $e->getFile(),
225
                'line' => $e->getLine(),
226
                'trace' => $e->getTraceAsString(),
227
            ]);
228
229
            if ($this->echoOutput) {
230
                cli()->error('Error during processing: '.$e->getMessage());
231
            }
232
        }
233
234
        $this->stats['duration'] = microtime(true) - $startTime;
235
236
        if ($this->echoOutput) {
237
            $this->outputStats();
238
        }
239
    }
240
241
    /**
242
     * Process a batch of releases using Laravel's native Concurrency for parallel execution.
243
     *
244
     * Note: Due to serialization limitations with DOMDocument and HtmlDomParser,
245
     * we process releases sequentially within the batch but can process multiple
246
     * batches concurrently using async tasks that create fresh instances.
247
     */
248
    protected function processBatch(array $batch): void
249
    {
250
        // Check if we can use async processing
251
        // For now, disable async to avoid child process issues with database connections
252
        // TODO: Re-enable async when database connection pooling is properly configured
253
        $useAsync = $this->canUseAsync() && config('nntmux.adult_processing_async', false);
254
255
        if (! $useAsync) {
256
            // Fall back to sequential processing (more reliable)
257
            foreach ($batch as $release) {
258
                $releaseId = (int) ($release['id'] ?? 0);
259
260
                if ($releaseId <= 0) {
261
                    Log::warning('Invalid release ID in batch: '.json_encode($release));
262
                    $this->stats['failed']++;
263
264
                    continue;
265
                }
266
267
                try {
268
                    $this->processReleaseItem($release);
269
                } catch (\Throwable $e) {
270
                    // processReleaseItem should handle its own exceptions, but this is a fallback
271
                    Log::error('Unexpected error in processBatch for release '.$releaseId.': '.$e->getMessage());
272
                    $this->stats['failed']++;
273
274
                    // Ensure the release is marked as processed to avoid infinite loop
275
                    try {
276
                        $this->updateReleaseXxxId($releaseId, -2);
277
                    } catch (\Throwable $updateError) {
278
                        Log::error('Failed to update release '.$releaseId.' with error code: '.$updateError->getMessage());
279
                    }
280
                }
281
            }
282
283
            return;
284
        }
285
286
        // For async processing using Laravel's native Concurrency facade
287
        // We create independent tasks with only serializable data
288
        $cookie = $this->cookie;
289
        $echoOutput = $this->echoOutput;
290
        $imgSavePath = $this->imgSavePath;
291
292
        $tasks = [];
293
        foreach ($batch as $idx => $release) {
294
            // Extract only the serializable data needed
295
            $releaseData = [
296
                'id' => $release['id'],
297
                'searchname' => $release['searchname'],
298
            ];
299
300
            $tasks[$idx] = fn () => self::processReleaseInChildProcess($releaseData, $cookie, $echoOutput, $imgSavePath);
301
        }
302
303
        try {
304
            $results = Concurrency::run($tasks);
305
306
            // Update stats based on results
307
            foreach ($results as $result) {
308
                if (is_array($result)) {
309
                    if ($result['matched']) {
310
                        $this->stats['matched']++;
311
                    }
312
                    if (isset($result['provider'])) {
313
                        $this->stats['providers'][$result['provider']] =
314
                            ($this->stats['providers'][$result['provider']] ?? 0) + 1;
315
                    }
316
                }
317
                $this->stats['processed']++;
318
            }
319
        } catch (\Throwable $e) {
320
            Log::error('Async batch processing failed: '.$e->getMessage());
321
322
            // Mark all releases in batch as processed with error to avoid infinite loop
323
            foreach ($batch as $release) {
324
                $this->stats['failed']++;
325
                try {
326
                    Release::query()->where('id', $release['id'])->update(['xxxinfo_id' => -2]);
327
                } catch (\Throwable $updateError) {
328
                    Log::error('Failed to mark release '.$release['id'].' as processed: '.$updateError->getMessage());
329
                }
330
            }
331
        }
332
    }
333
334
    /**
335
     * Process a release in a child process with fresh instances.
336
     * This is a static method to avoid serializing $this.
337
     */
338
    protected static function processReleaseInChildProcess(
339
        array $releaseData,
340
        string $cookie,
341
        bool $echoOutput,
342
        string $imgSavePath
343
    ): array {
344
        // Create fresh pipeline instance in child process
345
        $pipeline = new self([], $echoOutput);
346
        $pipeline->cookie = $cookie;
347
        $pipeline->imgSavePath = $imgSavePath;
348
349
        $result = [
350
            'id' => $releaseData['id'],
351
            'xxxinfo_id' => -2,
352
            'matched' => false,
353
            'provider' => null,
354
        ];
355
356
        $cleanTitle = $pipeline->parseXXXSearchName($releaseData['searchname']);
357
358
        if ($cleanTitle === false) {
359
            Release::query()->where('id', $releaseData['id'])->update(['xxxinfo_id' => -2]);
360
361
            return $result;
362
        }
363
364
        // Check if we already have this movie in the database
365
        $existingInfo = $pipeline->checkXXXInfoExists($cleanTitle);
366
367
        if ($existingInfo !== null) {
368
            $result['xxxinfo_id'] = (int) $existingInfo['id'];
369
            $result['matched'] = true;
370
        } else {
371
            $xxxId = $pipeline->updateXXXInfo($cleanTitle, $releaseData);
372
            $result['xxxinfo_id'] = $xxxId;
373
            $result['matched'] = $xxxId > 0;
374
375
            // Get the provider from the result
376
            if ($xxxId > 0) {
377
                $info = XxxInfo::find($xxxId);
378
                if ($info) {
379
                    $result['provider'] = $info->classused;
0 ignored issues
show
Bug introduced by
The property classused does not seem to exist on Illuminate\Database\Eloq...gHasThroughRelationship.
Loading history...
380
                }
381
            }
382
        }
383
384
        Release::query()->where('id', $releaseData['id'])->update(['xxxinfo_id' => $result['xxxinfo_id']]);
385
386
        return $result;
387
    }
388
389
    /**
390
     * Process a single release item.
391
     *
392
     * @return int XXX info ID or error code
393
     */
394
    protected function processReleaseItem(array $release): int
395
    {
396
        $idCheck = -2;
397
        $releaseId = (int) ($release['id'] ?? 0);
398
399
        if ($releaseId <= 0) {
400
            Log::warning('Invalid release ID in processReleaseItem');
401
            $this->stats['failed']++;
402
403
            return $idCheck;
404
        }
405
406
        try {
407
            $cleanTitle = $this->parseXXXSearchName($release['searchname'] ?? '');
408
409
            if ($cleanTitle === false) {
410
                if ($this->echoOutput) {
411
                    cli()->primary('.');
412
                }
413
                $this->updateReleaseXxxId($releaseId, $idCheck);
414
                $this->stats['skipped']++;
415
416
                return $idCheck;
417
            }
418
419
            // Check if we already have this movie in the database
420
            $existingInfo = $this->checkXXXInfoExists($cleanTitle);
421
422
            if ($existingInfo !== null) {
423
                if ($this->echoOutput) {
424
                    cli()->info('Local match found for XXX Movie: '.$cleanTitle);
425
                }
426
                $idCheck = (int) $existingInfo['id'];
427
            } else {
428
                if ($this->echoOutput) {
429
                    cli()->info('Looking up: '.$cleanTitle);
430
                    cli()->info('Local match not found, checking web!');
431
                }
432
433
                $idCheck = $this->updateXXXInfo($cleanTitle, $release);
434
435
                // If updateXXXInfo returned false, treat as -2
436
                if ($idCheck === false) {
0 ignored issues
show
introduced by
The condition $idCheck === false is always false.
Loading history...
437
                    $idCheck = -2;
438
                }
439
            }
440
441
            $this->updateReleaseXxxId($releaseId, $idCheck);
442
            $this->stats['processed']++;
443
444
            if ($idCheck > 0) {
445
                $this->stats['matched']++;
446
            }
447
        } catch (\Throwable $e) {
448
            Log::error('Processing failed for release '.$releaseId.': '.$e->getMessage(), [
449
                'exception' => get_class($e),
450
                'file' => $e->getFile(),
451
                'line' => $e->getLine(),
452
                'trace' => $e->getTraceAsString(),
453
            ]);
454
455
            // Still mark as processed with error code to avoid infinite loop
456
            try {
457
                $this->updateReleaseXxxId($releaseId, -2);
458
            } catch (\Throwable $updateError) {
459
                Log::error('Failed to update release '.$releaseId.' after processing error: '.$updateError->getMessage());
460
            }
461
            $this->stats['failed']++;
462
        }
463
464
        return $idCheck;
465
    }
466
467
    /**
468
     * Update XXX information from pipeline results.
469
     *
470
     * @return int|false XXX info ID or false on failure
471
     */
472
    public function updateXXXInfo(string $movie, ?array $release = null): int|false
0 ignored issues
show
Unused Code introduced by
The parameter $release is not used and could be removed. ( Ignorable by Annotation )

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

472
    public function updateXXXInfo(string $movie, /** @scrutinizer ignore-unused */ ?array $release = null): int|false

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
473
    {
474
        $result = $this->processMovie($movie);
475
476
        if ($result['status'] !== AdultProcessingResult::STATUS_MATCHED) {
477
            return -2;
478
        }
479
480
        $providerName = $result['provider'];
481
        $movieData = $result['movieData'];
482
483
        if ($this->echoOutput) {
484
            $fromStr = match ($providerName) {
485
                'aebn' => 'Adult Entertainment Broadcast Network',
486
                'ade' => 'Adult DVD Empire',
487
                'pop' => 'PopPorn',
488
                'adm' => 'Adult DVD Marketplace',
489
                'hotm' => 'HotMovies',
490
                default => $providerName,
491
            };
492
            cli()->primary('Fetching XXX info from: '.$fromStr);
493
        }
494
495
        // Track provider usage
496
        $this->stats['providers'][$providerName] = ($this->stats['providers'][$providerName] ?? 0) + 1;
497
498
        // Prepare the movie data for database storage
499
        $cast = ! empty($movieData['cast']) ? implode(',', (array) $movieData['cast']) : '';
500
        $genres = ! empty($movieData['genres']) ? $this->getGenreID($movieData['genres']) : '';
501
502
        $mov = [
503
            'trailers' => ! empty($movieData['trailers']) ? serialize($movieData['trailers']) : '',
504
            'extras' => ! empty($movieData['extras']) ? serialize($movieData['extras']) : '',
505
            'productinfo' => ! empty($movieData['productinfo']) ? serialize($movieData['productinfo']) : '',
506
            'backdrop' => ! empty($movieData['backcover']) ? $movieData['backcover'] : 0,
507
            'cover' => ! empty($movieData['boxcover']) ? $movieData['boxcover'] : 0,
508
            'title' => ! empty($movieData['title']) ? html_entity_decode($movieData['title'], ENT_QUOTES, 'UTF-8') : '',
509
            'plot' => ! empty($movieData['synopsis']) ? html_entity_decode($movieData['synopsis'], ENT_QUOTES, 'UTF-8') : '',
510
            'tagline' => ! empty($movieData['tagline']) ? html_entity_decode($movieData['tagline'], ENT_QUOTES, 'UTF-8') : '',
511
            'genre' => ! empty($genres) ? html_entity_decode($genres, ENT_QUOTES, 'UTF-8') : '',
512
            'director' => ! empty($movieData['director']) ? html_entity_decode($movieData['director'], ENT_QUOTES, 'UTF-8') : '',
513
            'actors' => ! empty($cast) ? html_entity_decode($cast, ENT_QUOTES, 'UTF-8') : '',
514
            'directurl' => ! empty($movieData['directurl']) ? html_entity_decode($movieData['directurl'], ENT_QUOTES, 'UTF-8') : '',
515
            'classused' => $providerName,
516
        ];
517
518
        $cover = 0;
519
        $backdrop = 0;
520
        $xxxID = false;
521
522
        // Check if this movie already exists
523
        $check = XxxInfo::query()->where('title', $mov['title'])->first(['id']);
524
525
        if ($check !== null && $check['id'] > 0) {
526
            $xxxID = $check['id'];
527
528
            // Update BoxCover
529
            if (! empty($mov['cover'])) {
530
                $cover = $this->releaseImage->saveImage($xxxID.'-cover', $mov['cover'], $this->imgSavePath);
531
            }
532
533
            // BackCover
534
            if (! empty($mov['backdrop'])) {
535
                $backdrop = $this->releaseImage->saveImage($xxxID.'-backdrop', $mov['backdrop'], $this->imgSavePath, 1920, 1024);
536
            }
537
538
            // Update existing record
539
            XxxInfo::query()->where('id', $check['id'])->update([
540
                'title' => $mov['title'],
541
                'tagline' => $mov['tagline'],
542
                'plot' => "\x1f\x8b\x08\x00".gzcompress($mov['plot']),
543
                'genre' => substr($mov['genre'], 0, 64),
544
                'director' => $mov['director'],
545
                'actors' => $mov['actors'],
546
                'extras' => $mov['extras'],
547
                'productinfo' => $mov['productinfo'],
548
                'trailers' => $mov['trailers'],
549
                'directurl' => $mov['directurl'],
550
                'classused' => $mov['classused'],
551
                'cover' => empty($cover) ? 0 : $cover,
552
                'backdrop' => empty($backdrop) ? 0 : $backdrop,
553
            ]);
554
        } else {
555
            // Insert new record
556
            $xxxID = XxxInfo::query()->insertGetId([
557
                'title' => $mov['title'],
558
                'tagline' => $mov['tagline'],
559
                'plot' => "\x1f\x8b\x08\x00".gzcompress($mov['plot']),
560
                'genre' => substr($mov['genre'], 0, 64),
561
                'director' => $mov['director'],
562
                'actors' => $mov['actors'],
563
                'extras' => $mov['extras'],
564
                'productinfo' => $mov['productinfo'],
565
                'trailers' => $mov['trailers'],
566
                'directurl' => $mov['directurl'],
567
                'classused' => $mov['classused'],
568
                'created_at' => now(),
569
                'updated_at' => now(),
570
            ]);
571
572
            // Update BoxCover
573
            if (! empty($mov['cover'])) {
574
                $cover = $this->releaseImage->saveImage($xxxID.'-cover', $mov['cover'], $this->imgSavePath);
0 ignored issues
show
Bug introduced by
Are you sure $xxxID of type Illuminate\Database\Eloquent\Builder|integer|mixed can be used in concatenation? ( Ignorable by Annotation )

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

574
                $cover = $this->releaseImage->saveImage(/** @scrutinizer ignore-type */ $xxxID.'-cover', $mov['cover'], $this->imgSavePath);
Loading history...
575
            }
576
577
            // BackCover
578
            if (! empty($mov['backdrop'])) {
579
                $backdrop = $this->releaseImage->saveImage($xxxID.'-backdrop', $mov['backdrop'], $this->imgSavePath, 1920, 1024);
580
            }
581
582
            XxxInfo::whereId($xxxID)->update(['cover' => $cover, 'backdrop' => $backdrop]);
583
        }
584
585
        if ($this->echoOutput) {
586
            cli()->primary(
587
                ($xxxID !== false ? 'Added/updated XXX movie: '.$mov['title'] : 'Nothing to update for XXX movie: '.$mov['title']),
588
                true
589
            );
590
        }
591
592
        return $xxxID;
593
    }
594
595
    /**
596
     * Get releases to process.
597
     */
598
    protected function getReleasesToProcess(): \Illuminate\Database\Eloquent\Collection
599
    {
600
        return Release::query()
601
            ->where(['xxxinfo_id' => 0])
602
            ->whereIn('categories_id', [
603
                Category::XXX_DVD,
604
                Category::XXX_WMV,
605
                Category::XXX_XVID,
606
                Category::XXX_X264,
607
                Category::XXX_SD,
608
                Category::XXX_CLIPHD,
609
                Category::XXX_CLIPSD,
610
                Category::XXX_WEBDL,
611
                Category::XXX_UHD,
612
                Category::XXX_VR,
613
            ])
614
            ->limit($this->movieQty)
615
            ->get(['searchname', 'id']);
616
    }
617
618
    /**
619
     * Check if async processing can be used.
620
     */
621
    protected function canUseAsync(): bool
622
    {
623
        return class_exists('Illuminate\Support\Facades\Concurrency') &&
624
               function_exists('pcntl_fork') &&
625
               ! defined('HHVM_VERSION');
626
    }
627
628
    /**
629
     * Check if XXX info already exists in database.
630
     */
631
    protected function checkXXXInfoExists(string $releaseName): ?array
632
    {
633
        $result = XxxInfo::query()->where('title', 'like', '%'.$releaseName.'%')->first(['id', 'title']);
634
635
        return $result ? $result->toArray() : null;
636
    }
637
638
    /**
639
     * Update release with XXX info ID.
640
     */
641
    protected function updateReleaseXxxId(int $releaseId, int $xxxInfoId): void
642
    {
643
        Release::query()->where('id', $releaseId)->update(['xxxinfo_id' => $xxxInfoId]);
644
    }
645
646
    /**
647
     * Get Genre ID from genre names.
648
     */
649
    protected function getGenreID(array|string $arr): string
650
    {
651
        $ret = null;
652
653
        if (! is_array($arr)) {
0 ignored issues
show
introduced by
The condition is_array($arr) is always true.
Loading history...
654
            $res = Genre::query()->where('title', $arr)->first(['id']);
655
            if ($res !== null) {
656
                return (string) $res['id'];
657
            }
658
659
            return '';
660
        }
661
662
        foreach ($arr as $value) {
663
            $res = Genre::query()->where('title', $value)->first(['id']);
664
            if ($res !== null) {
665
                $ret .= ','.$res['id'];
666
            } else {
667
                $ret .= ','.$this->insertGenre($value);
668
            }
669
        }
670
671
        return ltrim($ret, ',');
0 ignored issues
show
Bug introduced by
It seems like $ret can also be of type null; however, parameter $string of ltrim() 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

671
        return ltrim(/** @scrutinizer ignore-type */ $ret, ',');
Loading history...
672
    }
673
674
    /**
675
     * Insert a new genre.
676
     */
677
    protected function insertGenre(string $genre): int|string
678
    {
679
        if ($genre !== null) {
0 ignored issues
show
introduced by
The condition $genre !== null is always true.
Loading history...
680
            return Genre::query()->insertGetId([
0 ignored issues
show
Bug Best Practice introduced by
The expression return App\Models\Genre:...ROOT, 'disabled' => 0)) could return the type Illuminate\Database\Eloquent\Builder which is incompatible with the type-hinted return integer|string. Consider adding an additional type-check to rule them out.
Loading history...
681
                'title' => $genre,
682
                'type' => Category::XXX_ROOT,
683
                'disabled' => 0,
684
            ]);
685
        }
686
687
        return '';
688
    }
689
690
    /**
691
     * Parse XXX search name from release name.
692
     *
693
     * @return string|false Cleaned title or false if couldn't parse
694
     */
695
    protected function parseXXXSearchName(string $releaseName): string|false
696
    {
697
        $name = '';
698
        $followingList = '[^\w]((2160|1080|480|720)(p|i)|AC3D|Directors([^\w]CUT)?|DD5\.1|(DVD|BD|BR)(Rip)?|BluRay|divx|HDTV|iNTERNAL|LiMiTED|(Real\.)?Proper|RE(pack|Rip)|Sub\.?(fix|pack)|Unrated|WEB-DL|(x|H)[ ._-]?264|xvid|[Dd][Ii][Ss][Cc](\d+|\s*\d+|\.\d+)|XXX|BTS|DirFix|Trailer|WEBRiP|NFO|(19|20)\d\d)[^\w]';
699
700
        if (preg_match('/([^\w]{2,})?(?P<name>[\w .-]+?)'.$followingList.'/i', $releaseName, $hits)) {
701
            $name = $hits['name'];
702
        }
703
704
        if ($name !== '') {
705
            $name = preg_replace('/'.$followingList.'/i', ' ', $name);
706
            $name = preg_replace('/\(.*?\)|[._-]/i', ' ', $name);
707
            $name = trim(preg_replace('/\s{2,}/', ' ', $name));
708
            $name = trim(preg_replace('/^Private\s(Specials|Blockbusters|Blockbuster|Sports|Gold|Lesbian|Movies|Classics|Castings|Fetish|Stars|Pictures|XXX|Private|Black\sLabel|Black)\s\d+/i', '', $name));
709
            $name = trim(preg_replace('/(brazilian|chinese|croatian|danish|deutsch|dutch|estonian|flemish|finnish|french|german|greek|hebrew|icelandic|italian|latin|nordic|norwegian|polish|portuguese|japenese|japanese|russian|serbian|slovenian|spanish|spanisch|swedish|thai|turkish)$/i', '', $name));
710
711
            if (strlen($name) > 5 &&
712
                ! preg_match('/^\d+$/', $name) &&
713
                ! preg_match('/( File \d+ of \d+|\d+.\d+.\d+)/', $name) &&
714
                ! preg_match('/(E\d+)/', $name) &&
715
                ! preg_match('/\d\d\.\d\d.\d\d/', $name)
716
            ) {
717
                return $name;
718
            }
719
        }
720
721
        return false;
722
    }
723
724
    /**
725
     * Reset processing statistics.
726
     */
727
    protected function resetStats(): void
728
    {
729
        $this->stats = [
730
            'processed' => 0,
731
            'matched' => 0,
732
            'failed' => 0,
733
            'skipped' => 0,
734
            'duration' => 0.0,
735
            'providers' => [],
736
        ];
737
    }
738
739
    /**
740
     * Output processing statistics.
741
     */
742
    protected function outputStats(): void
743
    {
744
        cli()->header("\n=== Adult Processing Statistics ===");
745
        cli()->primary('Processed: '.$this->stats['processed']);
746
        cli()->primary('Matched: '.$this->stats['matched']);
747
        cli()->primary('Failed: '.$this->stats['failed']);
748
        cli()->primary('Skipped: '.$this->stats['skipped']);
749
        cli()->primary(sprintf('Duration: %.2f seconds', $this->stats['duration']));
750
751
        if (! empty($this->stats['providers'])) {
752
            cli()->header("\nProvider Statistics:");
753
            foreach ($this->stats['providers'] as $provider => $count) {
754
                cli()->primary("  {$provider}: {$count} matches");
755
            }
756
        }
757
    }
758
759
    /**
760
     * Get processing statistics.
761
     */
762
    public function getStats(): array
763
    {
764
        return $this->stats;
765
    }
766
767
    /**
768
     * Get all registered pipes.
769
     */
770
    public function getPipes(): Collection
771
    {
772
        return $this->pipes;
773
    }
774
}
775