Files
newznab-tmux-NNTmux/app/Services/AdditionalProcessing/ReleaseFilesArchiveFallback.php
T

284 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\AdditionalProcessing;
use App\Models\ReleaseFile;
use App\Services\AdditionalProcessing\Config\ProcessingConfiguration;
use App\Services\AdditionalProcessing\State\ReleaseProcessingContext;
use App\Services\AdditionalProcessing\Enums\DownloadKind;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Facades\File;
/**
* Shared fallback logic for extracting known files back out of downloaded archives.
*/
class ReleaseFilesArchiveFallback
{
public function __construct(
private readonly ProcessingConfiguration $config,
private readonly ArchiveExtractionService $archiveService,
private readonly MediaExtractionService $mediaService,
private readonly UsenetDownloadService $downloadService,
private readonly ReleaseFileManager $releaseManager,
private readonly ConsoleOutputService $output
) {}
/**
* Try JPG/PNG files listed inside an already-downloaded archive summary.
*
* @param array<int, array<string, mixed>> $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<int, ReleaseFile> $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<int, array<string, mixed>> $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<int, mixed> $segments
* @return list<string>
*/
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;
}
}