AdultProcessingPipeline::outputStats()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 13
rs 9.9332
cc 3
nc 3
nop 0
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 Blacklight\ColorCLI;
19
use Blacklight\ReleaseImage;
20
use Illuminate\Pipeline\Pipeline;
21
use Illuminate\Support\Collection;
22
use Illuminate\Support\Facades\Concurrency;
23
use Illuminate\Support\Facades\Log;
24
25
/**
26
 * Pipeline-based adult movie processing service using Laravel Pipeline.
27
 *
28
 * This service uses Laravel's Pipeline to orchestrate multiple adult movie data providers
29
 * to process releases and match them against movie metadata from various sources.
30
 *
31
 * It also supports parallel processing of multiple releases using Laravel's native Concurrency facade.
32
 */
33
class AdultProcessingPipeline
34
{
35
    /**
36
     * @var Collection<AbstractAdultProviderPipe>
37
     */
38
    protected Collection $pipes;
39
40
    protected int $movieQty;
41
    protected bool $echoOutput;
42
    protected ColorCLI $colorCli;
43
    protected ReleaseImage $releaseImage;
44
    protected string $imgSavePath;
45
    protected string $cookie;
46
    protected string $showPasswords;
47
48
    /**
49
     * Processing statistics.
50
     */
51
    protected array $stats = [
52
        'processed' => 0,
53
        'matched' => 0,
54
        'failed' => 0,
55
        'skipped' => 0,
56
        'duration' => 0.0,
57
        'providers' => [],
58
    ];
59
60
    /**
61
     * @param iterable<AbstractAdultProviderPipe> $pipes
62
     */
63
    public function __construct(iterable $pipes = [], bool $echoOutput = true)
64
    {
65
        $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

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

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

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

660
        return ltrim(/** @scrutinizer ignore-type */ $ret, ',');
Loading history...
661
    }
662
663
    /**
664
     * Insert a new genre.
665
     */
666
    protected function insertGenre(string $genre): int|string
667
    {
668
        if ($genre !== null) {
0 ignored issues
show
introduced by
The condition $genre !== null is always true.
Loading history...
669
            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...
670
                'title' => $genre,
671
                'type' => Category::XXX_ROOT,
672
                'disabled' => 0,
673
            ]);
674
        }
675
        return '';
676
    }
677
678
    /**
679
     * Parse XXX search name from release name.
680
     *
681
     * @return string|false Cleaned title or false if couldn't parse
682
     */
683
    protected function parseXXXSearchName(string $releaseName): string|false
684
    {
685
        $name = '';
686
        $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]';
687
688
        if (preg_match('/([^\w]{2,})?(?P<name>[\w .-]+?)' . $followingList . '/i', $releaseName, $hits)) {
689
            $name = $hits['name'];
690
        }
691
692
        if ($name !== '') {
693
            $name = preg_replace('/' . $followingList . '/i', ' ', $name);
694
            $name = preg_replace('/\(.*?\)|[._-]/i', ' ', $name);
695
            $name = trim(preg_replace('/\s{2,}/', ' ', $name));
696
            $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));
697
            $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));
698
699
            if (strlen($name) > 5 &&
700
                !preg_match('/^\d+$/', $name) &&
701
                !preg_match('/( File \d+ of \d+|\d+.\d+.\d+)/', $name) &&
702
                !preg_match('/(E\d+)/', $name) &&
703
                !preg_match('/\d\d\.\d\d.\d\d/', $name)
704
            ) {
705
                return $name;
706
            }
707
        }
708
709
        return false;
710
    }
711
712
    /**
713
     * Reset processing statistics.
714
     */
715
    protected function resetStats(): void
716
    {
717
        $this->stats = [
718
            'processed' => 0,
719
            'matched' => 0,
720
            'failed' => 0,
721
            'skipped' => 0,
722
            'duration' => 0.0,
723
            'providers' => [],
724
        ];
725
    }
726
727
    /**
728
     * Output processing statistics.
729
     */
730
    protected function outputStats(): void
731
    {
732
        $this->colorCli->header("\n=== Adult Processing Statistics ===");
733
        $this->colorCli->primary('Processed: ' . $this->stats['processed']);
734
        $this->colorCli->primary('Matched: ' . $this->stats['matched']);
735
        $this->colorCli->primary('Failed: ' . $this->stats['failed']);
736
        $this->colorCli->primary('Skipped: ' . $this->stats['skipped']);
737
        $this->colorCli->primary(sprintf('Duration: %.2f seconds', $this->stats['duration']));
738
739
        if (!empty($this->stats['providers'])) {
740
            $this->colorCli->header("\nProvider Statistics:");
741
            foreach ($this->stats['providers'] as $provider => $count) {
742
                $this->colorCli->primary("  {$provider}: {$count} matches");
743
            }
744
        }
745
    }
746
747
    /**
748
     * Get processing statistics.
749
     */
750
    public function getStats(): array
751
    {
752
        return $this->stats;
753
    }
754
755
    /**
756
     * Get all registered pipes.
757
     */
758
    public function getPipes(): Collection
759
    {
760
        return $this->pipes;
761
    }
762
}
763
764