> $files */ public function processJpgFromArchiveFileList( string $compressedData, array $files, ReleaseProcessingContext $context ): void { $imageFiles = array_values(array_filter( $files, static fn (array $file): bool => preg_match('/\.(jpe?g|png)$/i', (string) ($file['name'] ?? '')) === 1 )); if ($imageFiles === []) { return; } usort($imageFiles, static fn (array $a, array $b): int => ((int) ($b['size'] ?? 0)) <=> ((int) ($a['size'] ?? 0))); $this->processExtractedCandidatesFromData( $compressedData, array_slice($imageFiles, 0, 3), $context, 'extracted_', function (string $tempPath, ReleaseProcessingContext $context): bool { if (! $this->mediaService->isValidImage($tempPath)) { return false; } if (! $this->mediaService->getJPGSample($tempPath, $context->release->guid)) { return false; } $context->markFound('jpgSample'); $this->output->echoJpgSaved(); return true; } ); } /** * Re-download archives and extract stored JPG/PNG candidates. */ public function processJpgFromReleaseFiles(ReleaseProcessingContext $context): void { $imageFiles = ReleaseFile::query() ->where('releases_id', $context->release->id) ->where(function ($query) { $query->where('name', 'like', '%.jpg') ->orWhere('name', 'like', '%.jpeg') ->orWhere('name', 'like', '%.png'); }) ->orderByDesc('size') ->limit(3) ->get(); if ($imageFiles->isEmpty()) { return; } $this->processStoredReleaseFiles( $context, $imageFiles, 'release_file_', static fn (ReleaseProcessingContext $context): bool => $context->foundJPGSample, function (string $tempPath, ReleaseProcessingContext $context): bool { if (! $this->mediaService->isValidImage($tempPath)) { return false; } if (! $this->mediaService->getJPGSample($tempPath, $context->release->guid)) { return false; } $context->markFound('jpgSample'); $this->output->echoJpgSaved(); return true; } ); } /** * Re-download archives and extract stored NFO-like candidates. */ public function processNfoFromReleaseFiles(ReleaseProcessingContext $context): void { $nfoFiles = ReleaseFile::query() ->where('releases_id', $context->release->id) ->nfoFiles() ->limit(3) ->get(); if ($nfoFiles->isEmpty()) { return; } $nntp = $this->downloadService->getNNTP(); $this->processStoredReleaseFiles( $context, $nfoFiles, 'release_file_', static fn (ReleaseProcessingContext $context): bool => ! $context->releaseHasNoNFO, function (string $tempPath, ReleaseProcessingContext $context) use ($nntp): bool { if (! $this->releaseManager->processNfoFile($tempPath, $context, $nntp)) { return false; } $this->output->echoNfoFound(); return true; } ); } /** * @param EloquentCollection $candidates * @param callable(ReleaseProcessingContext): bool $shouldStop * @param callable(string, ReleaseProcessingContext): bool $handler */ private function processStoredReleaseFiles( ReleaseProcessingContext $context, EloquentCollection $candidates, string $tempPrefix, callable $shouldStop, callable $handler ): void { if ($candidates->isEmpty() || $context->nzbContents === []) { return; } foreach ($context->nzbContents as $nzbFile) { if ($shouldStop($context)) { break; } $title = (string) ($nzbFile['title'] ?? ''); if (preg_match( '/(\.(part0*1|rar|zip))(\s*\.rar)*($|[ ")]|-])|"[a-f0-9]{32}\.[1-9]\d{1,2}".*\(\d+\/\d{2,}\)$/i', $title ) !== 1) { continue; } $messageIDs = $this->extractInitialSegments($nzbFile['segments'] ?? []); if ($messageIDs === []) { continue; } $result = $this->downloadService->download( DownloadKind::Compressed, $messageIDs, $context->releaseGroupName, $context->release->id, $title ); if ($result['groupUnavailable']) { $context->groupUnavailable = true; $this->output->echoGroupUnavailable(); return; } if (! $result['success'] || empty($result['data'])) { continue; } foreach ($candidates as $candidate) { $fileData = $this->archiveService->extractSpecificFile( $result['data'], $candidate->name, $context->tmpPath ); if ($fileData === null || $fileData === '') { continue; } if ($this->withExtractedTempFile($candidate->name, $fileData, $context, $tempPrefix, $handler)) { return; } } } } /** * @param array> $candidates * @param callable(string, ReleaseProcessingContext): bool $handler */ private function processExtractedCandidatesFromData( string $compressedData, array $candidates, ReleaseProcessingContext $context, string $tempPrefix, callable $handler ): void { foreach ($candidates as $candidate) { $filename = (string) ($candidate['name'] ?? ''); if ($filename === '') { continue; } $fileData = $this->archiveService->extractSpecificFile($compressedData, $filename, $context->tmpPath); if ($fileData === null || $fileData === '') { continue; } if ($this->withExtractedTempFile($filename, $fileData, $context, $tempPrefix, $handler)) { return; } } } /** * @param callable(string, ReleaseProcessingContext): bool $handler */ private function withExtractedTempFile( string $filename, string $fileData, ReleaseProcessingContext $context, string $tempPrefix, callable $handler ): bool { $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $suffix = $extension === '' ? '' : '.'.$extension; $tempPath = $context->tmpPath.$tempPrefix.uniqid('', true).$suffix; File::put($tempPath, $fileData); try { return $handler($tempPath, $context); } finally { File::delete($tempPath); } } /** * @param array $segments * @return list */ private function extractInitialSegments(array $segments): array { $messageIDs = []; $segmentCount = min(count($segments), $this->config->maximumRarSegments); for ($index = 0; $index < $segmentCount; $index++) { $messageIDs[] = (string) $segments[$index]; } return $messageIDs; } }