Files
2026-08-11 10:43:22 +02:00

206 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\AdditionalProcessing;
use App\Services\AdditionalProcessing\Config\ProcessingConfiguration;
use App\Services\AdditionalProcessing\DTO\AdditionalWorkPlan;
use App\Services\AdditionalProcessing\DTO\ArchiveCandidate;
use Illuminate\Support\Facades\Log;
final readonly class AdditionalWorkPlanner
{
private const string ARCHIVE_PATTERN = '/(\.(part\d+|[rz]\d+|rar|0+|0*10?|zipr\d{2,3}|zipx?)("|\s*\.rar)*($|[ ")]|-])|"[a-f0-9]{32}\.[1-9]\d{1,2}".*\(\d+\/\d{2,}\)$)/i';
public function __construct(private ProcessingConfiguration $config) {}
/**
* @param array<int|string, mixed> $nzbContents
*/
public function plan(array $nzbContents, string $groupName): AdditionalWorkPlan
{
$sampleMessageIds = [];
$jpgMessageIds = [];
$mediaInfoMessageId = '';
$audioInfoMessageId = '';
$audioInfoExtension = '';
$archiveCandidates = [];
$bookFileCount = 0;
$duplicateMessageIdCount = 0;
$seenMessageIds = [];
foreach (array_values($nzbContents) as $sourceIndex => $file) {
if (! is_array($file)) {
continue;
}
try {
$title = (string) ($file['title'] ?? '');
$segments = is_array($file['segments'] ?? null) ? $file['segments'] : [];
if (preg_match($this->config->ignoreBookRegex, $title) === 1) {
$bookFileCount++;
}
if (preg_match(self::ARCHIVE_PATTERN, $title) === 1) {
$archiveMessageIds = $this->extractSegments(
$segments,
$this->config->maximumRarSegments,
$seenMessageIds,
$duplicateMessageIdCount,
);
if ($archiveMessageIds !== []) {
$archiveCandidates[] = new ArchiveCandidate(
title: $title,
messageIds: $archiveMessageIds,
likelyFirstVolume: $this->isLikelyFirstVolume($title),
sourceIndex: $sourceIndex,
);
}
}
if ($this->isSupportFile($title)) {
continue;
}
if ($this->config->processThumbnails && $sampleMessageIds === [] && $segments !== []
&& stripos($title, 'sample') !== false
&& preg_match('/\.(?:jpe?g|png|webp)$/i', $title) !== 1
) {
$sampleMessageIds = $this->extractSegments(
$segments,
$this->config->segmentsToDownload,
$seenMessageIds,
$duplicateMessageIdCount,
);
}
if ($this->config->processJPGSample && $jpgMessageIds === [] && $segments !== []
&& preg_match('/flac|lossless|mp3|music|inner-sanctum|sound/i', $groupName) !== 1
&& preg_match('/\.(?:jpe?g|png|webp)[. ")\]]/i', $title) === 1
) {
$jpgMessageIds = $this->extractSegments(
$segments,
$this->config->segmentsToDownload,
$seenMessageIds,
$duplicateMessageIdCount,
);
}
if ($this->config->processMediaInfo && $mediaInfoMessageId === '' && isset($segments[0])
&& stripos($title, 'sample') !== false
&& preg_match('/'.$this->config->videoFileRegex.'[. ")\]]/i', $title) === 1
) {
$mediaInfoMessageId = (string) $segments[0];
$this->recordMessageId($mediaInfoMessageId, $seenMessageIds, $duplicateMessageIdCount);
}
if ($this->config->processAudioInfo && $audioInfoMessageId === '' && isset($segments[0])
&& preg_match('/'.$this->config->audioFileRegex.'[. ")\]]/i', $title, $type) === 1
) {
$audioInfoExtension = (string) ($type[1] ?? '');
$audioInfoMessageId = (string) $segments[0];
$this->recordMessageId($audioInfoMessageId, $seenMessageIds, $duplicateMessageIdCount);
}
} catch (\ErrorException $e) {
Log::debug($e->getTraceAsString());
}
}
$bookFlood = $bookFileCount > 80 && ($bookFileCount * 2) >= count($nzbContents);
$unsupportedReasons = [];
if ($bookFlood) {
$unsupportedReasons[] = 'book-flood';
}
if ($archiveCandidates === []
&& $sampleMessageIds === []
&& $jpgMessageIds === []
&& $mediaInfoMessageId === ''
&& $audioInfoMessageId === ''
) {
$unsupportedReasons[] = 'no-supported-candidates';
}
return new AdditionalWorkPlan(
sampleMessageIds: $sampleMessageIds,
jpgMessageIds: $jpgMessageIds,
mediaInfoMessageId: $mediaInfoMessageId,
audioInfoMessageId: $audioInfoMessageId,
audioInfoExtension: $audioInfoExtension,
archiveCandidates: $archiveCandidates,
bookFileCount: $bookFileCount,
bookFlood: $bookFlood,
duplicateMessageIdCount: $duplicateMessageIdCount,
unsupportedReasons: $unsupportedReasons,
);
}
private function isSupportFile(string $title): bool
{
return preg_match(
'/(?:'.$this->config->supportFileRegex.'|nfo\b|inf\b|ofn\b)($|[ ")]|-])(?!.{20,})/i',
$title,
) === 1;
}
private function isLikelyFirstVolume(string $title): bool
{
if (preg_match('/\.part0*(\d+)/i', $title, $part) === 1) {
return (int) $part[1] === 1;
}
if (preg_match('/"[a-f0-9]{32}\.[1-9]\d{1,2}".*\((\d+)\/\d{2,}\)$/i', $title, $position) === 1) {
return (int) $position[1] === 1;
}
return preg_match('/\.(rar|zip)($|[ ")]|-])/i', $title) === 1;
}
/**
* @param array<int|string, mixed> $segments
* @param array<string, true> $seenMessageIds
* @return list<string>
*/
private function extractSegments(
array $segments,
int $limit,
array &$seenMessageIds,
int &$duplicateMessageIdCount,
): array {
$messageIds = [];
$requestMessageIds = [];
foreach (array_slice($segments, 0, max($limit, 0)) as $segment) {
$messageId = (string) $segment;
if ($messageId === '' || isset($requestMessageIds[$messageId])) {
if ($messageId !== '') {
$duplicateMessageIdCount++;
}
continue;
}
$requestMessageIds[$messageId] = true;
$messageIds[] = $messageId;
$this->recordMessageId($messageId, $seenMessageIds, $duplicateMessageIdCount);
}
return $messageIds;
}
/**
* @param array<string, true> $seenMessageIds
*/
private function recordMessageId(string $messageId, array &$seenMessageIds, int &$duplicateMessageIdCount): void
{
if (isset($seenMessageIds[$messageId])) {
$duplicateMessageIdCount++;
return;
}
$seenMessageIds[$messageId] = true;
}
}