NNTmux /
newznab-tmux
| 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
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
|
|||||
| 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
|
|||||
| 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
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
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
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
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
|
|||||
| 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
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
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
|
|||||
| 669 | return Genre::query()->insertGetId([ |
||||
|
0 ignored issues
–
show
|
|||||
| 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 |