Update nfo files handling

This commit is contained in:
DariusIII
2025-12-12 15:41:02 +01:00
parent 6744f1b14c
commit 4151ee91d5
6 changed files with 1551 additions and 85 deletions
+70 -10
View File
@@ -158,18 +158,78 @@ class NZBContents
}
// --- 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 explicit NFO files first (with enhanced patterns)
if ($nfoCheck && ! $foundNFO && isset($firstSegmentId)) {
// Standard NFO extensions
if (preg_match('/\.\b(nfo|diz|info?)\b(?![.-])/i', $subject)) {
$nfoMessageId = ['hidden' => false, 'id' => $firstSegmentId, 'priority' => 1];
$foundNFO = true;
}
// Alternative NFO naming patterns (group-specific or obfuscated)
elseif (preg_match('/(?:^|["\s])(?:file(?:_?id)?|readme|release|info(?:rmation)?|about|desc(?:ription)?|notes?|read\.?me|00-|000-|0-|_-_).*?\.(?:txt|nfo|diz)(?:["\s]|$)/i', $subject)) {
$nfoMessageId = ['hidden' => false, 'id' => $firstSegmentId, 'priority' => 2];
$foundNFO = true;
}
}
// Check for potential "hidden" NFOs (single segment, common name, not other known types)
// Check for potential "hidden" NFOs with improved detection
// Only consider this if an explicit NFO wasn't found yet
elseif ($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 ($nfoCheck && ! $foundNFO && ! $hiddenNFO && isset($firstSegmentId)) {
$isHiddenNfoCandidate = false;
// Pattern 1: Single segment files with (1/1)
if ($segmentCountInFile === 1 && preg_match('/\(1\/1\)$/i', $subject)) {
$isHiddenNfoCandidate = true;
}
// Pattern 2: Small segment count (1-2) with NFO-like names but no extension
if (! $isHiddenNfoCandidate && $segmentCountInFile <= 2 && preg_match('/(?:^|["\s])(?:nfo|info|readme|release|file_?id|about)(?:["\s]|$)/i', $subject)) {
$isHiddenNfoCandidate = true;
}
// Pattern 3: Scene-style NFO naming (group-release.nfo without extension visible)
if (! $isHiddenNfoCandidate && $segmentCountInFile === 1 && preg_match('/^[a-z0-9._-]+["\s]*\(1\/1\)/i', $subject)) {
// Check for scene-like naming pattern
if (preg_match('/^[a-z0-9]+[._-][a-z0-9._-]+["\s]*\(1\/1\)/i', $subject)) {
$isHiddenNfoCandidate = true;
}
}
// Pattern 4: Very small files (NFOs are typically small)
// Files described as very small in bytes could be NFOs
if (! $isHiddenNfoCandidate && $segmentCountInFile === 1 && preg_match('/yEnc\s*\(\d+\)\s*\[1\/1\]/i', $subject)) {
$isHiddenNfoCandidate = true;
}
if ($isHiddenNfoCandidate) {
// Enhanced exclusion: check if it's NOT likely another common file type
$excludedExtensions = '/\.(?:' .
// Executables
'exe|com|bat|cmd|scr|dll|msi|pkg|deb|rpm|apk|ipa|app|' .
// Archives
'zip|rar|[rst]\d{2}|7z|ace|tar|gz|bz2|xz|lzma|cab|iso|bin|cue|img|mdf|nrg|dmg|vhd|' .
// Audio
'mp3|flac|ogg|aac|wav|wma|m4a|opus|ape|wv|mpc|' .
// Video
'avi|mkv|mp4|mov|wmv|mpg|mpeg|ts|vob|m2ts|webm|flv|ogv|divx|xvid|' .
// Images
'jpg|jpeg|png|gif|bmp|tif|tiff|psd|webp|svg|ico|raw|cr2|nef|' .
// Documents
'pdf|doc|docx|xls|xlsx|ppt|pptx|odt|ods|odp|rtf|epub|mobi|azw|' .
// Code
'html|htm|css|js|php|py|java|c|cpp|h|cs|sql|json|xml|yml|yaml|' .
// Data
'db|dbf|mdb|accdb|sqlite|csv|' .
// Verification
'par2?|sfv|md5|sha1|sha256|sha512|crc|' .
// Misc
'url|lnk|cfg|ini|inf|sys|tmp|bak|log|srt|sub|idx|ass|ssa|vtt' .
')\b/i';
if (! preg_match($excludedExtensions, $subject)) {
$nfoMessageId = ['hidden' => true, 'id' => $firstSegmentId, 'priority' => 10];
$hiddenNFO = true;
}
}
}
+258 -1
View File
@@ -140,7 +140,14 @@ class NameFixer
/**
* Attempts to fix release names using the NFO.
*
* Enhanced to use the new Nfo class metadata extraction features for better
* release name identification from IMDB, TVDB, TMDB, and other media sources.
*
* @param int|string $time Time limit for query
* @param bool $echo Whether to actually update the database
* @param int $cats Category filter (2=misc/hashed, 3=predb)
* @param bool $nameStatus Whether to update status columns
* @param bool $show Whether to show output
*
* @throws \Exception
*/
@@ -149,6 +156,9 @@ class NameFixer
$this->_echoStartMessage($time, '.nfo files');
$type = 'NFO, ';
// Initialize the Nfo parser
$nfoParser = new Nfo();
// Only select releases we haven't checked here before
$preId = false;
if ($cats === 3) {
@@ -208,7 +218,18 @@ class NameFixer
}
$this->reset();
$this->checkName($releaseRow[0], $echo, $type, $nameStatus, $show, $preId);
// First, try to extract metadata using the enhanced Nfo parser
$nfoMetadata = $nfoParser->parseNfoMetadata($releaseRow[0]->textstring);
// Try to find a better name using extracted media IDs
$betterNameFound = $this->tryNfoMetadataRename($releaseRow[0], $nfoMetadata, $echo, $type, $nameStatus, $show, $preId);
// If metadata extraction didn't find a name, fall back to traditional checks
if (! $betterNameFound) {
$this->checkName($releaseRow[0], $echo, $type, $nameStatus, $show, $preId);
}
$this->_echoRenamed($show);
}
$this->_echoFoundCount($echo, ' NFO\'s');
@@ -217,6 +238,242 @@ class NameFixer
}
}
/**
* Try to rename a release using extracted NFO metadata.
*
* Uses media IDs (IMDB, TVDB, TMDB) and codec info from NFO to build
* a better release name.
*
* @param object $release The release object
* @param array $nfoMetadata Metadata extracted from NFO by Nfo::parseNfoMetadata()
* @param bool $echo Whether to update database
* @param string $type The type string for logging
* @param bool $nameStatus Whether to update status columns
* @param bool $show Whether to show output
* @param bool $preId Whether processing for PreDB
* @return bool True if a better name was found and applied
*
* @throws \Exception
*/
protected function tryNfoMetadataRename(object $release, array $nfoMetadata, bool $echo, string $type, $nameStatus, bool $show, bool $preId = false): bool
{
// Skip if already processed
if ($this->done || $this->relid === (int) $release->releases_id) {
return false;
}
// Try to get a name from media database IDs
$mediaIds = $nfoMetadata['media_ids'] ?? [];
$codecInfo = $nfoMetadata['codec_info'] ?? [];
$releaseGroup = $nfoMetadata['group'] ?? null;
// Priority order: IMDB (movies/TV), TMDB, TVDB, TVMaze
foreach ($mediaIds as $mediaId) {
$newName = $this->getNameFromMediaId($mediaId['source'], $mediaId['id']);
if ($newName !== null) {
// Enhance the name with codec info if available
$enhancedName = $this->enhanceNameWithCodecInfo($newName, $codecInfo, $releaseGroup);
$this->updateRelease(
$release,
$enhancedName,
'nfoCheck: Media ID ('.$mediaId['source'].': '.$mediaId['id'].')',
$echo,
$type,
$nameStatus,
$show
);
return true;
}
}
// If we have codec info but no media ID match, try to enhance existing name patterns
if (! empty($codecInfo)) {
// Check if there's a recognizable title pattern in the NFO
$titleFromNfo = $this->extractTitleFromNfoContent($release->textstring);
if ($titleFromNfo !== null) {
$enhancedName = $this->enhanceNameWithCodecInfo($titleFromNfo, $codecInfo, $releaseGroup);
if (strtolower($enhancedName) !== strtolower($release->searchname)) {
$this->updateRelease(
$release,
$enhancedName,
'nfoCheck: NFO Title with Codec Info',
$echo,
$type,
$nameStatus,
$show
);
return true;
}
}
}
return false;
}
/**
* Get a release name from a media database ID.
*
* Queries local database or external APIs to resolve media IDs to titles.
*
* @param string $source The source database (imdb, thetvdb, tmdb_movie, tmdb_tv, tvmaze, anidb, mal)
* @param string $id The media ID
* @return string|null The title if found, null otherwise
*/
protected function getNameFromMediaId(string $source, string $id): ?string
{
switch ($source) {
case 'imdb':
// Check if we have this IMDB ID in our movieinfo table
$movie = \App\Models\MovieInfo::where('imdbid', ltrim($id, 't'))->first(['title', 'year']);
if ($movie !== null) {
return $movie->year > 0 ? "{$movie->title} ({$movie->year})" : $movie->title;
}
// Also check Video table for TV shows with IMDB ID
$video = \App\Models\Video::where('imdb', (int) ltrim($id, 't'))->first(['title']);
if ($video !== null) {
return $video->title;
}
break;
case 'thetvdb':
// Check Video table for TVDB ID
$video = \App\Models\Video::where('tvdb', (int) $id)->first(['title']);
if ($video !== null) {
return $video->title;
}
break;
case 'tmdb_movie':
// Check local movie database for TMDB ID
$movie = \App\Models\MovieInfo::where('tmdbid', (int) $id)->first(['title', 'year']);
if ($movie !== null) {
return $movie->year > 0 ? "{$movie->title} ({$movie->year})" : $movie->title;
}
break;
case 'tmdb_tv':
// Check Video table for TMDB ID
$video = \App\Models\Video::where('tmdb', (int) $id)->first(['title']);
if ($video !== null) {
return $video->title;
}
break;
case 'tvmaze':
// Check Video table for TVMaze ID
$video = \App\Models\Video::where('tvmaze', (int) $id)->first(['title']);
if ($video !== null) {
return $video->title;
}
break;
case 'anidb':
// Check AniDB table - uses Video table with anidb column
$video = \App\Models\Video::where('anidb', (int) $id)->first(['title']);
if ($video !== null) {
return $video->title;
}
// Also check anidb_titles table
$anime = \App\Models\AnidbTitle::where('anidbid', (int) $id)->first(['title']);
if ($anime !== null) {
return $anime->title;
}
break;
case 'trakt':
// Check Video table for Trakt ID
$video = \App\Models\Video::where('trakt', (int) $id)->first(['title']);
if ($video !== null) {
return $video->title;
}
break;
case 'mal':
// MyAnimeList - currently no direct support in the database
break;
}
return null;
}
/**
* Enhance a title with codec/resolution information.
*
* @param string $title The base title
* @param array $codecInfo Codec info from Nfo::extractCodecInfo()
* @param string|null $releaseGroup The release group name if found
* @return string The enhanced title
*/
protected function enhanceNameWithCodecInfo(string $title, array $codecInfo, ?string $releaseGroup = null): string
{
$parts = [$title];
// Add resolution
if (! empty($codecInfo['resolution'])) {
$parts[] = $codecInfo['resolution'];
}
// Add video codec
if (! empty($codecInfo['video'])) {
$parts[] = $codecInfo['video'];
}
// Add audio codec
if (! empty($codecInfo['audio'])) {
$parts[] = $codecInfo['audio'];
}
// Add release group
if ($releaseGroup !== null) {
$parts[] = '-'.$releaseGroup;
return implode('.', array_slice($parts, 0, -1)).$parts[count($parts) - 1];
}
return implode('.', $parts);
}
/**
* Extract a recognizable title from NFO content.
*
* Looks for common title patterns like "Title (Year)" or scene-style names.
*
* @param string $nfoContent The NFO content
* @return string|null The extracted title or null if not found
*/
protected function extractTitleFromNfoContent(string $nfoContent): ?string
{
// Look for "Title (Year)" pattern - common in movie NFOs
if (preg_match('/^[\s\S]*?([A-Z][A-Za-z0-9\s\.\'\-\:]+(?:\s+\((?:19|20)\d{2}\)))/m', $nfoContent, $matches)) {
$title = trim($matches[1]);
// Validate it's not too short or too long
if (strlen($title) >= 5 && strlen($title) <= 150) {
return $title;
}
}
// Look for release name patterns (Scene style)
if (preg_match('/(?:Release|Rls|Name)\s*[:\-]?\s*([A-Za-z0-9][\w.\-]+(?:[\s._-][\w.\-]+)+)/i', $nfoContent, $matches)) {
$title = trim($matches[1]);
if (strlen($title) >= 5 && strlen($title) <= 150) {
return $title;
}
}
// Look for title in common NFO header patterns
if (preg_match('/(?:presents|proudly brings)\s*[:\-]?\s*([A-Za-z0-9][\w.\s\-\']+(?:[\s._-][\w.\s\-\']+)*)/i', $nfoContent, $matches)) {
$title = trim($matches[1]);
if (strlen($title) >= 5 && strlen($title) <= 150) {
return $title;
}
}
return null;
}
/**
* Attempts to fix release names using the File name.
*
+1044 -66
View File
File diff suppressed because it is too large Load Diff
@@ -626,12 +626,22 @@ class AdditionalProcessingOrchestrator
continue;
}
// NFO files
if ($context->releaseHasNoNFO && preg_match('/(\.(nfo|inf|ofn)|info\.txt)$/i', $filePath)) {
if ($this->releaseManager->processNfoFile($filePath, $context, $this->downloadService->getNNTP())) {
$this->output->echoNfoFound();
// NFO files - enhanced detection with multiple patterns
if ($context->releaseHasNoNFO) {
// Standard NFO extensions
if (preg_match('/(\.(nfo|inf|ofn|diz)|info\.txt)$/i', $filePath)) {
if ($this->releaseManager->processNfoFile($filePath, $context, $this->downloadService->getNNTP())) {
$this->output->echoNfoFound();
}
continue;
}
// Alternative NFO filenames (file_id.diz, readme.txt, etc.)
elseif ($this->releaseManager->isNfoFilename($filePath)) {
if ($this->releaseManager->processNfoFile($filePath, $context, $this->downloadService->getNNTP())) {
$this->output->echoNfoFound();
}
continue;
}
continue;
}
// Audio files
@@ -574,8 +574,82 @@ class ArchiveExtractionService
*/
private function getAllowedExtensions(): array
{
return ['nfo', 'srt', 'mkv', 'mpeg', 'avi', 'jpg', 'jpeg', 'exe', 'mp4', 'mp3', 'm4a',
'flac', 'png', 'epub', 'cbz', 'cbr', 'djvu'];
return [
// NFO and info files (prioritized for extraction)
'nfo', 'diz', 'inf', 'txt',
// Subtitles
'srt', 'sub', 'idx', 'ass', 'ssa', 'vtt',
// Video
'mkv', 'mpeg', 'avi', 'mp4', 'm4v', 'mov', 'wmv', 'flv', 'ts', 'vob', 'm2ts', 'webm',
// Audio
'mp3', 'm4a', 'flac', 'ogg', 'aac', 'wav', 'wma', 'opus', 'ape',
// Images
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp',
// Documents
'epub', 'pdf', 'cbz', 'cbr', 'djvu', 'mobi', 'azw', 'azw3',
// Executables (for software releases)
'exe', 'msi',
];
}
/**
* Check if a file is an NFO or info file.
*
* @param string $filename The filename to check.
* @return bool True if it's an NFO-like file.
*/
public function isNfoFile(string $filename): bool
{
$basename = strtolower(basename($filename));
// Standard NFO extensions
if (preg_match('/\.(nfo|diz|inf)$/i', $basename)) {
return true;
}
// Common NFO alternative names
$nfoNames = [
'file_id.diz', 'fileid.diz', 'file-id.diz',
'readme.txt', 'readme.1st', 'read.me', 'readmenow.txt',
'info.txt', 'information.txt', 'about.txt', 'notes.txt',
'release.txt', 'release.nfo',
];
if (in_array($basename, $nfoNames, true)) {
return true;
}
// Scene-style NFO naming: 00-groupname.nfo, group-release.nfo
if (preg_match('/^(?:00?-[a-z0-9_-]+|[a-z0-9]+-[a-z0-9._-]+)\.(?:nfo|txt)$/i', $basename)) {
return true;
}
return false;
}
/**
* Sort files to prioritize NFO files for processing.
*
* @param array $files Array of file info arrays.
* @return array Sorted array with NFO files first.
*/
public function sortFilesWithNfoPriority(array $files): array
{
usort($files, function ($a, $b) {
$aIsNfo = $this->isNfoFile($a['name'] ?? '');
$bIsNfo = $this->isNfoFile($b['name'] ?? '');
if ($aIsNfo && ! $bIsNfo) {
return -1;
}
if (! $aIsNfo && $bIsNfo) {
return 1;
}
return 0;
});
return $files;
}
/**
@@ -345,7 +345,12 @@ class ReleaseFileManager
}
/**
* Process NFO file.
* Process NFO file with enhanced detection capabilities.
*
* Supports multiple NFO naming conventions:
* - Standard: .nfo, .diz, .info
* - Alternative: file_id.diz, readme.txt, info.txt
* - Scene-style: 00-groupname.nfo, groupname-releasename.nfo
*/
public function processNfoFile(
string $fileLocation,
@@ -354,6 +359,10 @@ class ReleaseFileManager
): bool {
try {
$data = File::get($fileLocation);
// Try to detect and convert encoding
$data = $this->normalizeNfoEncoding($data);
if ($this->nfo->isNFO($data, $context->release->guid)
&& $this->nfo->addAlternateNfo($data, $context->release, $nntp)
) {
@@ -367,6 +376,84 @@ class ReleaseFileManager
return false;
}
/**
* Check if a filename looks like an NFO file.
*
* @param string $filename The filename to check.
* @return bool True if the filename matches NFO patterns.
*/
public function isNfoFilename(string $filename): bool
{
// Standard NFO extensions
if (preg_match('/\.(?:nfo|diz|info?)$/i', $filename)) {
return true;
}
// Alternative NFO filenames
$nfoPatterns = [
'/^(?:file[_-]?id|readme|release|info(?:rmation)?|about|notes?)\.(?:txt|diz)$/i',
'/^00-[a-z0-9_-]+\.nfo$/i', // Scene: 00-group.nfo
'/^0+-[a-z0-9_-]+\.nfo$/i', // Scene variations
'/^[a-z0-9_-]+-[a-z0-9_.-]+\.nfo$/i', // Scene: group-release.nfo
'/info\.txt$/i', // info.txt (common alternative)
];
$basename = basename($filename);
foreach ($nfoPatterns as $pattern) {
if (preg_match($pattern, $basename)) {
return true;
}
}
return false;
}
/**
* Normalize NFO encoding to UTF-8.
*
* NFO files often use CP437 (DOS) encoding for ASCII art.
* This method attempts to detect and convert various encodings.
*
* @param string $data Raw NFO data.
* @return string UTF-8 encoded NFO data.
*/
protected function normalizeNfoEncoding(string $data): string
{
// Check for UTF-8 BOM and remove it
if (str_starts_with($data, "\xEF\xBB\xBF")) {
$data = substr($data, 3);
}
// Check for UTF-16 BOM
if (str_starts_with($data, "\xFF\xFE")) {
// UTF-16 LE
$data = mb_convert_encoding(substr($data, 2), 'UTF-8', 'UTF-16LE');
} elseif (str_starts_with($data, "\xFE\xFF")) {
// UTF-16 BE
$data = mb_convert_encoding(substr($data, 2), 'UTF-8', 'UTF-16BE');
}
// If already valid UTF-8, return as-is
if (mb_check_encoding($data, 'UTF-8')) {
return $data;
}
// Try CP437 (DOS encoding - common for scene NFOs with ASCII art)
// Use the utility function if available
if (class_exists('\Blacklight\utility\Utility') && method_exists('\Blacklight\utility\Utility', 'cp437toUTF')) {
return \Blacklight\utility\Utility::cp437toUTF($data);
}
// Fallback: try ISO-8859-1 (Latin-1)
$converted = @mb_convert_encoding($data, 'UTF-8', 'ISO-8859-1');
if ($converted !== false) {
return $converted;
}
// Last resort: force UTF-8 with error handling
return mb_convert_encoding($data, 'UTF-8', 'UTF-8');
}
/**
* Handle release name extraction from RAR file content.
*/