From 478d25e7a29ba5b9964b8b8b440d42cec6d5f2b6 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Fri, 9 Jan 2026 13:37:52 +0100 Subject: [PATCH] Improve jpg samples fetching --- .../AdditionalProcessingOrchestrator.php | 168 +++++++++++++++ .../ArchiveExtractionService.php | 202 ++++++++++++++++++ 2 files changed, 370 insertions(+) diff --git a/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php b/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php index e2ce02313..f7f04139d 100644 --- a/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php +++ b/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php @@ -3,6 +3,7 @@ namespace App\Services\AdditionalProcessing; use App\Models\Release; +use App\Models\ReleaseFile; use App\Models\UsenetGroup; use App\Services\AdditionalProcessing\Config\ProcessingConfiguration; use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; @@ -229,6 +230,11 @@ class AdditionalProcessingOrchestrator $this->processExtractedFiles($context); } } + + // If still no JPG sample, try to fetch JPG from release_files entries + if (! $context->foundJPGSample && $this->config->processJPGSample) { + $this->processJpgFromReleaseFiles($context); + } } // Finalize @@ -562,9 +568,171 @@ class AdditionalProcessingOrchestrator $this->releaseManager->updateSearchIndex($context->release->id); } + // Try to extract and process JPG files from the archive file list for sample/preview + if (! $context->foundJPGSample && $this->config->processJPGSample) { + $this->processJpgFromArchiveFileList($compressedData, $result['files'], $context); + } + return $context->totalFileInfo > 0; } + /** + * Find and process JPG files from archive file list to create sample/preview. + */ + private function processJpgFromArchiveFileList( + string $compressedData, + array $files, + ReleaseProcessingContext $context + ): void { + // Find JPG files in the archive file list + $jpgFiles = []; + foreach ($files as $file) { + $name = $file['name'] ?? ''; + if (preg_match('/\.jpe?g$/i', $name)) { + $jpgFiles[] = $file; + } + } + + if (empty($jpgFiles)) { + return; + } + + // Sort by size (prefer larger images, likely better quality) - limit to first 3 + usort($jpgFiles, fn ($a, $b) => ($b['size'] ?? 0) <=> ($a['size'] ?? 0)); + $jpgFiles = array_slice($jpgFiles, 0, 3); + + foreach ($jpgFiles as $jpgFile) { + $jpgFilename = $jpgFile['name']; + + // Try to extract this specific JPG from the archive + $jpgData = $this->archiveService->extractSpecificFile( + $compressedData, + $jpgFilename, + $context->tmpPath + ); + + if ($jpgData === null || empty($jpgData)) { + continue; + } + + // Save to temp file and validate it's actually a JPEG + $tempJpgPath = $context->tmpPath.'extracted_'.uniqid('', true).'.jpg'; + File::put($tempJpgPath, $jpgData); + + if ($this->mediaService->isJpegData($tempJpgPath)) { + if ($this->mediaService->getJPGSample($tempJpgPath, $context->release->guid)) { + $context->foundJPGSample = true; + $this->output->echoJpgSaved(); + File::delete($tempJpgPath); + break; + } + } + + File::delete($tempJpgPath); + } + } + + /** + * Process JPG files from existing release_files entries. + * This fetches the archive from NZB and extracts JPG files that were previously listed. + */ + private function processJpgFromReleaseFiles(ReleaseProcessingContext $context): void + { + // Get JPG files from release_files table for this release + $jpgFiles = ReleaseFile::where('releases_id', $context->release->id) + ->where(function ($query) { + $query->where('name', 'like', '%.jpg') + ->orWhere('name', 'like', '%.jpeg'); + }) + ->orderByDesc('size') + ->limit(3) + ->get(); + + if ($jpgFiles->isEmpty()) { + return; + } + + // We need to download a compressed file from the NZB to extract the JPG + if (empty($context->nzbContents)) { + return; + } + + // Find compressed files in NZB + foreach ($context->nzbContents as $nzbFile) { + if ($context->foundJPGSample) { + break; + } + + $title = $nzbFile['title'] ?? ''; + if (! preg_match( + '/(\\.(part0*1|rar|zip|7z))(\\s*\\.rar)*($|[ ")]|-])|"[a-f0-9]{32}\\.[1-9]\\d{1,2}".*\\(\\d+\\/\\d{2,}\\)$/i', + $title + )) { + continue; + } + + // Get message IDs for first few segments + $segments = $nzbFile['segments'] ?? []; + if (empty($segments)) { + continue; + } + + $messageIDs = []; + $segCount = min(count($segments), $this->config->maximumRarSegments); + for ($i = 0; $i < $segCount; $i++) { + $messageIDs[] = (string) $segments[$i]; + } + + if (empty($messageIDs)) { + continue; + } + + // Download the compressed file + $result = $this->downloadService->downloadCompressedFile( + $messageIDs, + $context->releaseGroupName, + $context->release->id, + $title + ); + + if (! $result['success'] || empty($result['data'])) { + continue; + } + + // Try to extract each JPG file from the archive + foreach ($jpgFiles as $jpgFile) { + if ($context->foundJPGSample) { + break; + } + + $jpgData = $this->archiveService->extractSpecificFile( + $result['data'], + $jpgFile->name, + $context->tmpPath + ); + + if ($jpgData === null || empty($jpgData)) { + continue; + } + + // Save to temp file and validate + $tempJpgPath = $context->tmpPath.'release_file_'.uniqid('', true).'.jpg'; + File::put($tempJpgPath, $jpgData); + + if ($this->mediaService->isJpegData($tempJpgPath)) { + if ($this->mediaService->getJPGSample($tempJpgPath, $context->release->guid)) { + $context->foundJPGSample = true; + $this->output->echoJpgSaved(); + File::delete($tempJpgPath); + break 2; // Exit both loops + } + } + + File::delete($tempJpgPath); + } + } + } + /** * Process extracted files from archives. */ diff --git a/app/Services/AdditionalProcessing/ArchiveExtractionService.php b/app/Services/AdditionalProcessing/ArchiveExtractionService.php index 23135b21d..a5b60ecc7 100644 --- a/app/Services/AdditionalProcessing/ArchiveExtractionService.php +++ b/app/Services/AdditionalProcessing/ArchiveExtractionService.php @@ -783,6 +783,208 @@ class ArchiveExtractionService return $this->archiveInfo; } + /** + * Extract a specific file from archive data by filename. + * + * @param string $compressedData The raw archive data + * @param string $filename The filename to extract (exact match) + * @param string $tmpPath Temporary directory path + * @return string|null The extracted file content, or null if extraction failed + */ + public function extractSpecificFile(string $compressedData, string $filename, string $tmpPath): ?string + { + // Try using ArchiveInfo's built-in extraction + if ($this->archiveInfo->setData($compressedData, true)) { + try { + $extracted = $this->archiveInfo->getFileData($filename); + if ($extracted !== false && ! empty($extracted)) { + return $extracted; + } + } catch (\Throwable $e) { + if ($this->config->debugMode) { + Log::debug('ArchiveInfo getFileData failed: '.$e->getMessage()); + } + } + } + + // Fallback: use external tools to extract to temp directory + $archiveType = $this->detectArchiveType($compressedData); + + if ($archiveType === '7z' && $this->config->sevenZipPath) { + return $this->extractFileVia7zip($compressedData, $filename, $tmpPath); + } + + // Try using unrar for RAR files + if ($this->config->unrarPath) { + $extracted = $this->extractFileViaUnrar($compressedData, $filename, $tmpPath); + if ($extracted !== null) { + return $extracted; + } + } + + // Try using unzip for ZIP files + if ($this->config->unzipPath) { + $extracted = $this->extractFileViaUnzip($compressedData, $filename, $tmpPath); + if ($extracted !== null) { + return $extracted; + } + } + + return null; + } + + /** + * Extract a specific file using 7zip. + */ + private function extractFileVia7zip(string $compressedData, string $filename, string $tmpPath): ?string + { + try { + $extractDir = $tmpPath.'extract_'.uniqid('', true).'/'; + if (! File::isDirectory($extractDir)) { + File::makeDirectory($extractDir, 0777, true, true); + } + + $archiveFile = $tmpPath.'archive_'.uniqid('', true).'.7z'; + File::put($archiveFile, $compressedData); + + // Extract specific file + $cmd = [$this->config->sevenZipPath, 'e', '-y', '-bd', '-o'.$extractDir, $archiveFile, $filename]; + $exitCode = 0; + $stdout = null; + $stderr = null; + $this->execCommand($cmd, $exitCode, $stdout, $stderr); + + File::delete($archiveFile); + + // Look for extracted file + $extractedPath = $extractDir.basename($filename); + if (File::isFile($extractedPath)) { + $content = File::get($extractedPath); + File::deleteDirectory($extractDir); + + return $content; + } + + // Try to find it with glob (in case path differs) + $files = File::allFiles($extractDir); + foreach ($files as $file) { + if (strtolower($file->getFilename()) === strtolower(basename($filename))) { + $content = File::get($file->getPathname()); + File::deleteDirectory($extractDir); + + return $content; + } + } + + File::deleteDirectory($extractDir); + } catch (\Throwable $e) { + if ($this->config->debugMode) { + Log::debug('7zip extraction failed: '.$e->getMessage()); + } + } + + return null; + } + + /** + * Extract a specific file using unrar. + */ + private function extractFileViaUnrar(string $compressedData, string $filename, string $tmpPath): ?string + { + try { + $extractDir = $tmpPath.'extract_'.uniqid('', true).'/'; + if (! File::isDirectory($extractDir)) { + File::makeDirectory($extractDir, 0777, true, true); + } + + $archiveFile = $tmpPath.'archive_'.uniqid('', true).'.rar'; + File::put($archiveFile, $compressedData); + + // Extract specific file using unrar + $killString = $this->config->getKillString(); + runCmd($killString.$this->config->unrarPath.'" e -y -c- -inul -p- "'.$archiveFile.'" "'.$filename.'" "'.$extractDir.'"'); + + File::delete($archiveFile); + + // Look for extracted file + $extractedPath = $extractDir.basename($filename); + if (File::isFile($extractedPath)) { + $content = File::get($extractedPath); + File::deleteDirectory($extractDir); + + return $content; + } + + // Try to find it with glob + $files = File::allFiles($extractDir); + foreach ($files as $file) { + if (strtolower($file->getFilename()) === strtolower(basename($filename))) { + $content = File::get($file->getPathname()); + File::deleteDirectory($extractDir); + + return $content; + } + } + + File::deleteDirectory($extractDir); + } catch (\Throwable $e) { + if ($this->config->debugMode) { + Log::debug('Unrar extraction failed: '.$e->getMessage()); + } + } + + return null; + } + + /** + * Extract a specific file using unzip. + */ + private function extractFileViaUnzip(string $compressedData, string $filename, string $tmpPath): ?string + { + try { + $extractDir = $tmpPath.'extract_'.uniqid('', true).'/'; + if (! File::isDirectory($extractDir)) { + File::makeDirectory($extractDir, 0777, true, true); + } + + $archiveFile = $tmpPath.'archive_'.uniqid('', true).'.zip'; + File::put($archiveFile, $compressedData); + + // Extract specific file using unzip + runCmd($this->config->unzipPath.' -j "'.$archiveFile.'" "'.$filename.'" -d "'.$extractDir.'"'); + + File::delete($archiveFile); + + // Look for extracted file + $extractedPath = $extractDir.basename($filename); + if (File::isFile($extractedPath)) { + $content = File::get($extractedPath); + File::deleteDirectory($extractDir); + + return $content; + } + + // Try to find it with glob + $files = File::allFiles($extractDir); + foreach ($files as $file) { + if (strtolower($file->getFilename()) === strtolower(basename($filename))) { + $content = File::get($file->getPathname()); + File::deleteDirectory($extractDir); + + return $content; + } + } + + File::deleteDirectory($extractDir); + } catch (\Throwable $e) { + if ($this->config->debugMode) { + Log::debug('Unzip extraction failed: '.$e->getMessage()); + } + } + + return null; + } + /** * Execute a command with output capture. */