From 3390e84d32aabcdaa25186055147e8a2796b61de Mon Sep 17 00:00:00 2001 From: DariusIII Date: Tue, 29 Apr 2025 12:56:52 +0200 Subject: [PATCH] Update code --- Blacklight/NZBContents.php | 257 ++++++++---- Blacklight/Nfo.php | 388 +++++++++++------- .../processing/post/ProcessAdditional.php | 15 +- 3 files changed, 412 insertions(+), 248 deletions(-) diff --git a/Blacklight/NZBContents.php b/Blacklight/NZBContents.php index 0d3638815..cbb598ce0 100755 --- a/Blacklight/NZBContents.php +++ b/Blacklight/NZBContents.php @@ -45,144 +45,219 @@ class NZBContents } /** - * Look for an .nfo file in the NZB, return the NFO message id. + * Look for an .nfo file in the NZB, download it, verify it, and return the content. * + * @param string $guid The release GUID. + * @param int $relID The release ID. + * @param int $groupID The group ID. + * @param string $groupName The group name. * - * @return bool|mixed + * @return string|false The verified NFO content as a string, or false if not found, download failed, or verification failed. * - * @throws \Exception + * @throws \Exception If NNTP operations fail. */ - public function getNfoFromNZB($guid, $relID, $groupID, $groupName): mixed + public function getNfoFromNZB(string $guid, int $relID, int $groupID, string $groupName): string|false { - $fetchedBinary = false; - + // Step 1: Attempt to find a potential NFO message ID $messageID = $this->parseNZB($guid, $relID, $groupID, true); - if ($messageID !== false) { - $fetchedBinary = $this->nntp->getMessages($groupName, $messageID['id'], $this->alternateNNTP); - if ($this->nntp->isError($fetchedBinary)) { - // NFO download failed, increment attempts. - Release::query()->where('id', $relID)->decrement('nfostatus'); - if ($this->echooutput) { - echo 'f'; - } - return false; - } - if ($this->nfo->isNFO($fetchedBinary, $guid) === true) { - if ($this->echooutput) { - echo $messageID['hidden'] === false ? '+' : '*'; - } - } else { - if ($this->echooutput) { - echo '-'; - } - Release::query()->where('id', $relID)->update(['nfostatus' => Nfo::NFO_NONFO]); - $fetchedBinary = false; - } - } else { + // If no NFO message ID found + if ($messageID === false || !isset($messageID['id'])) { if ($this->echooutput) { echo '-'; } + // Make sure we set status to NFO_NONFO Release::query()->where('id', $relID)->update(['nfostatus' => Nfo::NFO_NONFO]); + return false; } - return $fetchedBinary; + // Step 2: Attempt to download the potential NFO content + $fetchedBinary = $this->nntp->getMessages($groupName, $messageID['id'], $this->alternateNNTP); + + // Check if download failed + if ($this->nntp->isError($fetchedBinary)) { + // NFO download failed, decrement attempts to allow retries + Release::query()->where('id', $relID)->decrement('nfostatus'); + if ($this->echooutput) { + echo 'f'; + } + return false; + } + + // Step 3: Verify if the downloaded content is actually an NFO file + if ($this->nfo->isNFO($fetchedBinary, $guid)) { + // NFO verification successful + if ($this->echooutput) { + // Show if it was found via explicit name (+) or potentially hidden (*) + echo $messageID['hidden'] === false ? '+' : '*'; + } + return $fetchedBinary; + } + + // Step 4: Handle verification failure - not a valid NFO + if ($this->echooutput) { + echo '-'; + } + Release::query()->where('id', $relID)->update(['nfostatus' => Nfo::NFO_NONFO]); + return false; } /** * Gets the completion from the NZB, optionally looks if there is an NFO/PAR2 file. * + * This version includes improved regex for PAR2 file detection. * - * @return array|false + * @param string $guid The release GUID. + * @param int $relID The release ID. + * @param int $groupID The group ID. + * @param bool $nfoCheck Whether to specifically look for an NFO file. * - * @throws \Exception + * @return array|false An array containing NFO message ID and hidden status, or false if not found/error. + * + * @throws \Exception If NNTP operations fail. */ public function parseNZB($guid, $relID, $groupID, bool $nfoCheck = false): bool|array { - $nzbFile = $this->LoadNZB($guid); - if ($nzbFile !== false) { - $messageID = $hiddenID = ''; - $actualParts = $artificialParts = 0; - $foundPAR2 = $this->lookuppar2 === false; - $foundNFO = $hiddenNFO = $nfoCheck === false; + $nzbFile = $this->loadNzb($guid); + if ($nzbFile === false) { + return false; + } - foreach ($nzbFile->file as $nzbcontents) { - foreach ($nzbcontents->segments->segment as $segment) { - $actualParts++; + $messageID = $hiddenID = ''; + $actualParts = $artificialParts = 0; + // Initialize foundPAR2 based on settings; if lookuppar2 is false, we don't need to find one. + $foundPAR2 = $this->lookuppar2 === false; + // Initialize NFO flags based on whether we are checking for NFOs. + $foundNFO = $hiddenNFO = $nfoCheck === false; + $nfoMessageId = null; // Store potential NFO message ID here + + foreach ($nzbFile->file as $nzbContents) { + $segmentCountInFile = 0; + $firstSegmentId = null; // Initialize here for each file + foreach ($nzbContents->segments->segment as $segment) { + $actualParts++; + $segmentCountInFile++; + // Store the first segment ID of the current file, potentially useful for NFO/PAR2 + if ($segmentCountInFile === 1) { + $firstSegmentId = (string) $segment; } + } - $subject = (string) $nzbcontents->attributes()->subject; - if (preg_match('/(\d+)\)$/', $subject, $parts)) { - $artificialParts += $parts[1]; - } + $subject = (string) $nzbContents->attributes()->subject; + if (preg_match('/(?:[(\[])?(\d+)[\/)\\]](\d+)[)\]]?$/', $subject, $parts)) { + // Improve artificial parts calculation robustness (e.g., "[15/20]", "(15/20)") + if (isset($parts[2]) && (int)$parts[2] > 0) { + // Use the total count from the subject if available and seems valid + $artificialParts += (int)$parts[2]; + } + } else if (preg_match('/(\d+)\)$/', $subject, $parts)) { + // Fallback to original simple check if the more robust one fails + $artificialParts += (int)$parts[1]; + } - if (($foundNFO === false) && preg_match('/\.\b(nfo|inf|ofn)\b(?![ .-])/i', $subject)) { - $messageID = (string) $nzbcontents->segments->segment; - $foundNFO = true; - } - if ($foundNFO === false && $hiddenNFO === false && preg_match('/\(1\/1\)$/i', $subject) && ! preg_match('/\.(apk|bat|bmp|cbr|cbz|cfg|css|csv|cue|db|dll|doc|epub|exe|gif|htm|ico|idx|ini'.'|jpg|lit|log|m3u|mid|mobi|mp3|nib|nzb|odt|opf|otf|par|par2|pdf|psd|pps|png|ppt|r\d{2,4}'.'|rar|sfv|srr|sub|srt|sql|rom|rtf|tif|torrent|ttf|txt|vb|vol\d+\+\d+|wps|xml|zip)/i', $subject)) { - $hiddenID = (string) $nzbcontents->segments->segment; - $hiddenNFO = true; - } + // --- NFO Detection --- + // Check for explicit NFO files first + if ($nfoCheck && !$foundNFO && isset($firstSegmentId) && preg_match('/\.\b(nfo|diz|info?)\b(?![.-])/i', $subject)) { + $nfoMessageId = ['hidden' => false, 'id' => $firstSegmentId]; + $foundNFO = true; // Found an explicit NFO, prioritize this + } + // Check for potential "hidden" NFOs (single segment, common name, not other known types) + // Only consider this if an explicit NFO wasn't found yet + else if ($nfoCheck && !$foundNFO && !$hiddenNFO && isset($firstSegmentId) && $segmentCountInFile === 1 && preg_match('/\(1\/1\)$/i', $subject)) { + // Simplified exclusion: check if it's NOT likely another common file type based on extension pattern + if (!preg_match('/\.(?:exe|com|bat|cmd|scr|dll|zip|rar|[rst]\d{2}|[a-z0-9]{3}|7z|ace|tar|gz|bz2|iso|bin|cue|img|mdf|nrg|dmg|vhd|mp3|flac|ogg|aac|wav|wma|avi|mkv|mp4|mov|wmv|mpg|mpeg|ts|vob|jpg|jpeg|png|gif|bmp|tif|tiff|psd|pdf|doc|docx|xls|xlsx|ppt|pptx|txt|log|xml|html|css|js|php|py|java|c|cpp|h|cs|sql|db|dbf|mdb|accdb|par2?|sfv|md5|sha1|sha256|url|lnk|cfg|ini|inf|sys|tmp|bak|msi|pkg|deb|rpm|apk|ipa)\b/i', $subject)) { + $nfoMessageId = ['hidden' => true, 'id' => $firstSegmentId]; + $hiddenNFO = true; // Found a potential hidden NFO + } + } - if ($foundPAR2 === false && preg_match('/\.(par[&2" ]|\d{2,3}").+\(1\/1\)$/i', $subject) && $this->pp->parsePAR2((string) $nzbcontents->segments->segment, $relID, $groupID, $this->nntp, 1) === true) { - Release::query()->where('id', $relID)->update(['proc_par2' => 1]); + // --- PAR2 Detection --- + // Look specifically for the .par2 index file (often small, but not always 1/1) + if ($this->lookuppar2 && !$foundPAR2 && isset($firstSegmentId) && preg_match('/\.par2$/i', $subject)) { + // Attempt to parse the PAR2 file using its first segment ID + // Ensure $this->pp is initialized and parsePAR2 exists and accepts these parameters + if (method_exists($this->pp, 'parsePAR2') && $this->pp->parsePAR2($firstSegmentId, $relID, $groupID, $this->nntp, 1) === true) { + Release::query()->where('id', $relID)->update(['proc_par2' => 1]); // Use constant if available $foundPAR2 = true; } } + } // End foreach $nzbFile->file - if ($artificialParts === 0 || $actualParts === 0) { - $completion = 0; - } else { - $completion = ($actualParts / $artificialParts) * 100; - } - if ($completion > 100) { - $completion = 100; - } - - Release::query()->where('id', $relID)->update(['completion' => $completion]); - - if ($foundNFO === true && \strlen($messageID) > 1) { - return ['hidden' => false, 'id' => $messageID]; - } - - if ($hiddenNFO === true && \strlen($hiddenID) > 1) { - return ['hidden' => true, 'id' => $hiddenID]; - } + // Calculate completion + // Avoid division by zero and handle cases where parts info might be missing/incorrect + if ($artificialParts > 0) { + $completion = min(100, ($actualParts / $artificialParts) * 100); + } else if ($actualParts > 0) { + // If artificial parts couldn't be determined, but we have actual parts, + // we can't calculate completion accurately based on subject. + // Consider if $actualParts alone means 100% or if it's unknown. + // Setting to 100 if actual parts > 0 and artificial is 0 might be misleading. + // Let's default to 0 or another state indicating unknown completion from subject. + $completion = 0; // Or potentially set a specific status? + } + else { + // If both are zero (e.g., empty NZB or parsing issue), completion is 0. + $completion = 0; } + Release::query()->where('id', $relID)->update(['completion' => $completion]); + + // If NFO check was requested, return the found message ID (prioritizing explicit) + if ($nfoCheck && $nfoMessageId !== null && isset($nfoMessageId['id']) && \strlen($nfoMessageId['id']) > 1) { + return $nfoMessageId; + } + + // If NFO check was requested but nothing suitable was found + if ($nfoCheck && $nfoMessageId === null) { + // Update status to indicate no NFO was found in the NZB structure + Release::query()->where('id', $relID)->update(['nfostatus' => Nfo::NFO_NONFO]); + return false; + } + + // If NFO check was not requested, the function's primary goal might be just completion/PAR2 update. + // The original function returned false here. Decide if true (parsed successfully) or false is more appropriate. + // Returning false maintains original behavior when nfoCheck is false and no NFO ID is returned. return false; } - public function LoadNZB($guid): \SimpleXMLElement|bool + /** + * Loads and parses an NZB file based on a GUID. + * + * @param string $guid The release GUID to locate the NZB file + * + * @return \SimpleXMLElement|bool The parsed NZB file as SimpleXMLElement or false on failure + */ + public function loadNzb(string $guid): \SimpleXMLElement|bool { - // Fetch the NZB location using the GUID. + // Fetch the NZB file path using the GUID $nzbPath = $this->nzb->NZBPath($guid); if ($nzbPath === false) { return false; } - $nzbContents = Utility::unzipGzipFile($nzbPath); - if (! $nzbContents) { - if ($this->echooutput) { - echo PHP_EOL. - 'Unable to decompress: '. - $nzbPath. - ' - '. - fileperms($nzbPath). - ' - may have bad file permissions, skipping.'. - PHP_EOL; - } + // Attempt to decompress the NZB file + $nzbContents = Utility::unzipGzipFile($nzbPath); + if (empty($nzbContents)) { + if ($this->echooutput) { + $perms = fileperms($nzbPath); + $formattedPerms = $perms !== false ? decoct($perms & 0777) : 'unknown'; + echo PHP_EOL . "Unable to decompress: {$nzbPath} - {$formattedPerms} - may have bad file permissions, skipping." . PHP_EOL; + } return false; } - $nzbFile = @simplexml_load_string($nzbContents); - if (! $nzbFile) { - if ($this->echooutput) { - echo PHP_EOL."Unable to load NZB: $guid appears to be an invalid NZB, skipping.".PHP_EOL; - } + // Safely parse the XML content + libxml_use_internal_errors(true); + $nzbFile = simplexml_load_string($nzbContents); + if ($nzbFile === false) { + if ($this->echooutput) { + $errors = libxml_get_errors(); + $errorMsg = !empty($errors) ? " - XML error: " . $errors[0]->message : ""; + echo PHP_EOL . "Unable to load NZB: {$guid} appears to be an invalid NZB{$errorMsg}, skipping." . PHP_EOL; + libxml_clear_errors(); + } return false; } @@ -197,7 +272,7 @@ class NZBContents */ public function checkPAR2(string $guid, int $relID, int $groupID, int $nameStatus, int $show): bool { - $nzbFile = $this->LoadNZB($guid); + $nzbFile = $this->loadNzb($guid); if ($nzbFile !== false) { foreach ($nzbFile->file as $nzbContents) { if ($nameStatus === 1 && $this->pp->parsePAR2((string) $nzbContents->segments->segment, $relID, $groupID, $this->nntp, $show) && preg_match('/\.(par[2" ]|\d{2,3}").+\(1\/1\)/i', (string) $nzbContents->attributes()->subject)) { diff --git a/Blacklight/Nfo.php b/Blacklight/Nfo.php index 58f7337a9..82dbaad29 100755 --- a/Blacklight/Nfo.php +++ b/Blacklight/Nfo.php @@ -13,12 +13,38 @@ use dariusiii\rarinfo\SfvInfo; use getID3; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Log; +use Throwable; /** * Class Nfo. */ class Nfo { + /** + * Regex to detect common non-NFO file headers/signatures. + * @var string + */ + protected string $_nonNfoHeaderRegex = '/\A(\s*<\?xml|=newz\[NZB\]=|RIFF|\s*[RP]AR|.{0,10}(JFIF|matroska|ftyp|ID3))|;\s*Generated\s*by.*SF\w/i'; + + /** + * Regex to identify text encoding from the 'file' command output. + * @var string + */ + protected string $_textFileRegex = '/(ASCII|ISO-8859|UTF-(8|16|32).*?)\s*text/'; + + /** + * Regex to identify common binary file types from the 'file' command output. + * @var string + */ + protected string $_binaryFileRegex = '/^(JPE?G|Parity|PNG|RAR|XML|(7-)?[Zz]ip)/'; + + /** + * Regex to detect binary characters within the content. + * @var string + */ + protected string $_binaryCharsRegex = '/[\x00-\x08\x12-\x1F\x0B\x0E\x0F]/'; + /** * @var int */ @@ -120,63 +146,80 @@ class Nfo /** * Confirm this is an NFO file. * - * @param string|bool $possibleNFO The nfo. + * @param bool|string $possibleNFO The nfo content. * @param string $guid The guid of the release. - * @return bool True on success, False on failure. - * - * @throws \Exception + * @return bool True if it's likely an NFO, False otherwise. */ - public function isNFO(&$possibleNFO, string $guid): bool + public function isNFO(bool|string &$possibleNFO, string $guid): bool { if ($possibleNFO === false) { return false; } - // Make sure it's not too big or small, size needs to be at least 12 bytes for header checking. Ignore common file types. $size = \strlen($possibleNFO); - if ($size < 65535 && - $size > 11 && - ! preg_match( - '/\A(\s*<\?xml|=newz\[NZB\]=|RIFF|\s*[RP]AR|.{0,10}(JFIF|matroska|ftyp|ID3))|;\s*Generated\s*by.*SF\w/i', - $possibleNFO - )) { + + // Basic size and signature checks + if ($size >= 65535 || $size < 12 || preg_match($this->_nonNfoHeaderRegex, $possibleNFO)) { + return false; + } + + $tmpPath = $this->tmpPath . $guid . '.nfo'; + $isNfo = false; // Default assumption + + try { // File/GetId3 work with files, so save to disk. - $tmpPath = $this->tmpPath.$guid.'.nfo'; File::put($tmpPath, $possibleNFO); - // Linux boxes have 'file' (so should Macs), Windows *can* have it too: see GNUWIN.txt in docs. + // Use 'file' command via Utility::fileInfo if available $result = Utility::fileInfo($tmpPath); - if (! empty($result)) { - // Check if it's text. - if (preg_match('/(ASCII|ISO-8859|UTF-(8|16|32).*?)\s*text/', $result)) { - @File::delete($tmpPath); - - return true; - - // Or binary. - } - - if (preg_match('/^(JPE?G|Parity|PNG|RAR|XML|(7-)?[Zz]ip)/', $result) || preg_match('/[\x00-\x08\x12-\x1F\x0B\x0E\x0F]/', $possibleNFO)) { - @File::delete($tmpPath); - - return false; + if (!empty($result)) { + if (preg_match($this->_textFileRegex, $result)) { + $isNfo = true; // It's text, likely NFO + } elseif (preg_match($this->_binaryFileRegex, $result) || preg_match($this->_binaryCharsRegex, $possibleNFO)) { + $isNfo = false; // Detected binary format or characters } + // If fileInfo gave a result, trust it and return + return $isNfo; } + // Fallback checks if 'file' command is unavailable or inconclusive // Check if it's a par2. - $par2info = new Par2Info; + $par2info = new Par2Info(); $par2info->setData($possibleNFO); - if ($par2info->error) { - // Check if it's an SFV. - $sfv = new SfvInfo; - $sfv->setData($possibleNFO); - if ($sfv->error) { - return true; + if (!$par2info->error) { + // It's a PAR2 file + return false; + } + + // Check if it's an SFV. + $sfv = new SfvInfo(); + $sfv->setData($possibleNFO); + if (!$sfv->error) { + // It's an SFV file + return false; + } + + // If it wasn't identified as a known non-NFO binary type by fileInfo, + // and isn't PAR2 or SFV, assume it might be NFO (especially if fileInfo failed). + // Further checks (like binary char check) could be added here if needed. + $isNfo = !preg_match($this->_binaryCharsRegex, $possibleNFO); + + } catch (Throwable $e) { + // Log errors during file operations + Log::error("Error processing potential NFO for GUID {$guid}: " . $e->getMessage()); + $isNfo = false; // Treat errors as non-NFO + } finally { + // Ensure temporary file is always deleted + if (File::exists($tmpPath)) { + try { + File::delete($tmpPath); + } catch (Throwable $e) { + Log::error("Error deleting temporary NFO file {$tmpPath}: " . $e->getMessage()); } } } - return false; + return $isNfo; } /** @@ -222,158 +265,199 @@ class Nfo return false; } - /** + /** * Attempt to find NFO files inside the NZB's of releases. * - * @param string $groupID (optional) Group ID. - * @param string $guidChar (optional) First character of the release GUID (used for multi-processing). - * @param int $processImdb (optional) Attempt to find IMDB id's in the NZB? - * @param int $processTv (optional) Attempt to find Tv id's in the NZB? - * @return int How many NFO's were processed? + * @param NNTP $nntp The NNTP connection object + * @param string $groupID (optional) Group ID to filter releases by + * @param string $guidChar (optional) First character of the GUID for parallel processing + * @param bool $processImdb (optional) Process IMDB IDs (currently unused) + * @param bool $processTv (optional) Process TV IDs (currently unused) * - * @throws \Exception + * @return int Count of successfully processed NFO files + * + * @throws \Exception If NNTP operations fail */ - public function processNfoFiles($nntp, string $groupID = '', string $guidChar = '', int $processImdb = 1, int $processTv = 1): int + public function processNfoFiles(NNTP $nntp, string $groupID = '', string $guidChar = '', bool $processImdb = true, bool $processTv = true): int { - $ret = 0; + $processedCount = 0; - $qry = Release::query() - ->where('nzbstatus', '=', NZB::NZB_ADDED) - ->whereBetween('nfostatus', [$this->maxRetries, self::NFO_UNPROC]); + // Build base query with all filters + $baseQuery = $this->buildNfoProcessingQuery($groupID, $guidChar); - if ($guidChar !== '') { - $qry->where('leftguid', $guidChar); - } - if ($groupID !== '') { - $qry->where('groups_id', $groupID); - } - - if ($this->maxSize > 0) { - $qry->where('size', '<', $this->maxSize * 1073741824); - } - - if ($this->minSize > 0) { - $qry->where('size', '>', $this->minSize * 1048576); - } - - $res = $qry + // Fetch releases to process + $releases = $baseQuery->clone() ->orderBy('nfostatus') ->orderByDesc('postdate') ->limit($this->nzbs) ->get(['id', 'guid', 'groups_id', 'name']); - $nfoCount = $res->count(); + $nfoCount = $releases->count(); if ($nfoCount > 0) { - $this->colorCli->primary( - PHP_EOL. - ($guidChar === '' ? '' : '['.$guidChar.'] '). - ($groupID === '' ? '' : '['.$groupID.'] '). - 'Processing '.$nfoCount. - ' NFO(s), starting at '.$this->nzbs. - ' * = hidden NFO, + = NFO, - = no NFO, f = download failed.' - ); + // Display processing information + $this->displayProcessingHeader($guidChar, $groupID, $nfoCount); + // Show detailed stats if echo is enabled if ($this->echo) { - // Get count of releases per nfo status - $qry = Release::query() - ->where('nzbstatus', '=', NZB::NZB_ADDED) - ->whereBetween('nfostatus', [$this->maxRetries, self::NFO_UNPROC]) - ->select(['nfostatus as status', DB::raw('COUNT(id) as count')]) - ->groupBy(['nfostatus']) - ->orderBy('nfostatus'); - - if ($guidChar !== '') { - $qry->where('leftguid', $guidChar); - } - if ($groupID !== '') { - $qry->where('groups_id', $groupID); - } - - if ($this->maxSize > 0) { - $qry->where('size', '<', $this->maxSize * 1073741824); - } - - if ($this->minSize > 0) { - $qry->where('size', '>', $this->minSize * 1048576); - } - - $nfoStats = $qry->get(); - - if ($nfoStats instanceof \Traversable) { - $outString = PHP_EOL.'Available to process'; - foreach ($nfoStats as $row) { - $outString .= ', '.$row['status'].' = '.number_format($row['count']); - } - $this->colorCli->header($outString.'.'); - } + $this->displayNfoStatusStats($baseQuery); } - $nzbContents = new NZBContents( - [ - 'Echo' => $this->echo, - 'NNTP' => $nntp, - 'Nfo' => $this, - 'Settings' => null, - 'PostProcess' => new PostProcess(['Echo' => $this->echo, 'Nfo' => $this]), - ] - ); - $movie = new Movie(['Echo' => $this->echo]); + // Process each release + $nzbContents = new NZBContents(['NNTP' => $nntp, 'Nfo' => $this]); - foreach ($res as $arr) { - $fetchedBinary = $nzbContents->getNfoFromNZB($arr['guid'], $arr['id'], $arr['groups_id'], UsenetGroup::getNameByID($arr['groups_id'])); - if ($fetchedBinary !== false) { - // Insert nfo into database. + foreach ($releases as $release) { + try { + $groupName = UsenetGroup::getNameByID($release['groups_id']); + $fetchedBinary = $nzbContents->getNfoFromNZB($release['guid'], $release['id'], $release['groups_id'], $groupName); - $ckReleaseId = ReleaseNfo::whereReleasesId($arr['id'])->first(['releases_id']); - if ($ckReleaseId === null) { - ReleaseNfo::query()->insert(['releases_id' => $arr['id'], 'nfo' => "\x1f\x8b\x08\x00".gzcompress($fetchedBinary)]); + if ($fetchedBinary !== false) { + DB::beginTransaction(); + try { + // Only insert if not already present + $exists = ReleaseNfo::whereReleasesId($release['id'])->exists(); + if (!$exists) { + ReleaseNfo::query()->insert([ + 'releases_id' => $release['id'], + 'nfo' => "\x1f\x8b\x08\x00".gzcompress($fetchedBinary) + ]); + } + + // Update status + Release::whereId($release['id'])->update(['nfostatus' => self::NFO_FOUND]); + DB::commit(); + $processedCount++; + } catch (\Exception $e) { + DB::rollBack(); + if ($this->echo) { + $this->colorCli->error("Error saving NFO for release {$release['id']}: {$e->getMessage()}"); + } + } + } + } catch (\Exception $e) { + if ($this->echo) { + $this->colorCli->error("Error processing release {$release['id']}: {$e->getMessage()}"); } - Release::whereId($arr['id'])->update(['nfostatus' => self::NFO_FOUND]); - $ret++; - // Disable for now, we don't want to do full processing on nfo's. - /* $movie->doMovieUpdate($fetchedBinary, 'nfo', $arr['id'], $processImdb); */ - - // If set scan for tv info. - // Disable for now, we don't want to do full processing on nfo's. - /*if ($processTv === 1) { - (new PostProcess(['Echo' => $this->echo]))->processTv($groupID, $guidChar, $processTv); - }*/ } } } - // Remove nfo that we cant fetch after 5 attempts. - $qry = Release::query() + // Process failed NFO attempts + $this->handleFailedNfoAttempts($groupID, $guidChar); + + // Output results + if ($this->echo) { + if ($nfoCount > 0) { + echo PHP_EOL; + } + if ($processedCount > 0) { + $this->colorCli->primary($processedCount.' NFO file(s) found/processed.'); + } + } + + return $processedCount; + } + + /** + * Build base query for NFO processing with all common filters + */ + private function buildNfoProcessingQuery(string $groupID, string $guidChar): \Illuminate\Database\Eloquent\Builder + { + $query = Release::query() + ->where('nzbstatus', '=', NZB::NZB_ADDED) + ->whereBetween('nfostatus', [$this->maxRetries, self::NFO_UNPROC]); + + if ($guidChar !== '') { + $query->where('leftguid', $guidChar); + } + + if ($groupID !== '') { + $query->where('groups_id', $groupID); + } + + if ($this->maxSize > 0) { + $query->where('size', '<', $this->maxSize * 1073741824); + } + + if ($this->minSize > 0) { + $query->where('size', '>', $this->minSize * 1048576); + } + + return $query; + } + + /** + * Display header information about the NFO processing + */ + private function displayProcessingHeader(string $guidChar, string $groupID, int $nfoCount): void + { + $this->colorCli->primary( + PHP_EOL. + ($guidChar === '' ? '' : '['.$guidChar.'] '). + ($groupID === '' ? '' : '['.$groupID.'] '). + 'Processing '.$nfoCount. + ' NFO(s), starting at '.$this->nzbs. + ' * = hidden NFO, + = NFO, - = no NFO, f = download failed.' + ); + } + + /** + * Display statistics about NFO status counts + */ + private function displayNfoStatusStats(\Illuminate\Database\Eloquent\Builder $baseQuery): void + { + $nfoStats = $baseQuery->clone() + ->select(['nfostatus as status', DB::raw('COUNT(id) as count')]) + ->groupBy(['nfostatus']) + ->orderBy('nfostatus') + ->get(); + + if ($nfoStats instanceof \Traversable && $nfoStats->count() > 0) { + $outString = PHP_EOL.'Available to process'; + foreach ($nfoStats as $row) { + $outString .= ', '.$row['status'].' = '.number_format($row['count']); + } + $this->colorCli->header($outString.'.'); + } + } + + /** + * Handle releases that have failed too many NFO fetch attempts + */ + private function handleFailedNfoAttempts(string $groupID, string $guidChar): void + { + $failedQuery = Release::query() ->where('nzbstatus', NZB::NZB_ADDED) ->where('nfostatus', '<', $this->maxRetries) ->where('nfostatus', '>', self::NFO_FAILED); if ($guidChar !== '') { - $qry->where('leftguid', $guidChar); + $failedQuery->where('leftguid', $guidChar); } + if ($groupID !== '') { - $qry->where('groups_id', $groupID); + $failedQuery->where('groups_id', $groupID); } - foreach ($qry->get(['id']) as $release) { - // remove any releasenfo for failed - ReleaseNfo::whereReleasesId($release['id'])->delete(); + // Process in chunks to avoid memory issues with large result sets + $failedQuery->select(['id'])->chunk(100, function ($releases) { + DB::beginTransaction(); + try { + foreach ($releases as $release) { + // Remove any releasenfo for failed attempts + ReleaseNfo::whereReleasesId($release->id)->delete(); - // set release.nfostatus to failed - Release::whereId($release['id'])->update(['nfostatus' => self::NFO_FAILED]); - } - - if ($this->echo) { - if ($nfoCount > 0) { - echo PHP_EOL; + // Set release.nfostatus to failed + Release::whereId($release->id)->update(['nfostatus' => self::NFO_FAILED]); + } + DB::commit(); + } catch (\Exception $e) { + DB::rollBack(); + if ($this->echo) { + $this->colorCli->error("Error handling failed NFO attempts: {$e->getMessage()}"); + } } - if ($ret > 0) { - $this->colorCli->primary($ret.' NFO file(s) found/processed.'); - } - } - - return $ret; + }); } /** diff --git a/Blacklight/processing/post/ProcessAdditional.php b/Blacklight/processing/post/ProcessAdditional.php index be9f7c713..f126ef0b2 100755 --- a/Blacklight/processing/post/ProcessAdditional.php +++ b/Blacklight/processing/post/ProcessAdditional.php @@ -1949,13 +1949,18 @@ class ProcessAdditional } /** - * @throws \Exception + * @throws \Exception Potentially thrown by addAlternateNfo */ - protected function _processNfoFile($fileLocation): void + protected function _processNfoFile(string $fileLocation): void { - $data = @File::get($fileLocation); - if ($data != false && $this->_nfo->isNFO($data, $this->_release->guid) && $this->_nfo->addAlternateNfo($data, $this->_release, $this->_nntp)) { - $this->_releaseHasNoNFO = false; + try { + $data = File::get($fileLocation); + if ($this->_nfo->isNFO($data, $this->_release->guid) && $this->_nfo->addAlternateNfo($data, $this->_release, $this->_nntp)) { + $this->_releaseHasNoNFO = false; + } + } catch (FileNotFoundException $e) { + // Log or handle the case where the file doesn't exist or isn't readable + Log::warning("Could not read potential NFO file: {$fileLocation} - {$e->getMessage()}"); } }