*/ private const IGNORABLE_NAME_PATTERNS = [ '/^(?:audio|video)[._-]?ts$/i', '/^vts[._-]?\d{1,2}[._-]?\d{1,2}$/i', '/^\d{1,3}$/', ]; /** * Prioritize files for name matching. * * Returns files sorted by usefulness for name matching: * 1. SRR files (contain original release name) * 2. Main RAR files (not split parts) * 3. First split RAR parts * 4. Video files * 5. NFO files * 6. Other files * * @param array $files Array of filenames * @return array Sorted array of filenames */ public function prioritizeForMatching(array $files): array { $categorized = $this->categorizeFiles($files); // Merge in priority order return array_merge( $categorized['srr'], $categorized['mainRar'], $categorized['firstPart'], $categorized['video'], $categorized['nfo'], $categorized['other'] ); } /** * Prioritize files for PreDB matching. * * Similar to prioritizeForMatching but with slightly different priorities * optimized for PreDB filename lookups. * * @param array $files Array of filenames * @return array Sorted array of filenames */ public function prioritizeForPreDb(array $files): array { $categorized = $this->categorizeFiles($files); // For PreDB, SRR files are most valuable, then main RARs return array_merge( $categorized['srr'], $categorized['mainRar'], $categorized['firstPart'], $categorized['video'], $categorized['other'] ); } /** * Categorize files into groups by type. * * @param array $files Array of filenames * @return array Categorized files */ protected function categorizeFiles(array $files): array { $categories = [ 'srr' => [], 'mainRar' => [], 'firstPart' => [], 'video' => [], 'nfo' => [], 'other' => [], ]; foreach ($files as $file) { $lowerFile = strtolower($file); // Skip junk files that do not carry release identity. if ($this->shouldIgnoreForNameMatching($file) || $this->isSampleOrProof($file)) { continue; } // Categorize by type if ($this->isSrrFile($lowerFile)) { $categories['srr'][] = $file; } elseif ($this->isMainRar($lowerFile)) { $categories['mainRar'][] = $file; } elseif ($this->isFirstSplitRar($lowerFile)) { $categories['firstPart'][] = $file; } elseif ($this->isVideoFile($lowerFile)) { $categories['video'][] = $file; } elseif ($this->isNfoFile($lowerFile)) { $categories['nfo'][] = $file; } else { $categories['other'][] = $file; } } // Sort video files by length (longer names often more descriptive) usort($categories['video'], fn ($a, $b) => strlen($b) - strlen($a)); usort($categories['mainRar'], fn ($a, $b) => strlen($b) - strlen($a)); return $categories; } /** * Get priority value for a filename based on CRC matching. * * @param string $filename The filename to check * @return int Priority value (lower = higher priority) */ public function getCrcPriority(string $filename): int { $lower = strtolower($filename); if ($this->isSampleOrProof($filename)) { return self::PRIORITY_SAMPLE; } if ($this->isVideoFile($lower)) { return self::PRIORITY_VIDEO; } if ($this->isMainRar($lower)) { return self::PRIORITY_MAIN_RAR; } if ($this->isFirstSplitRar($lower)) { return self::PRIORITY_FIRST_PART_RAR; } if (preg_match('/\.(rar|r\d{2,3})$/i', $filename)) { return self::PRIORITY_OTHER_RAR; } if ($this->isNfoFile($lower)) { return self::PRIORITY_NFO; } return self::PRIORITY_OTHER; } /** * Check if a file is a sample or proof file. */ protected function isSampleOrProof(string $filename): bool { return (bool) preg_match('/[\.\-_](sample|proof|subs?|thumbs?)[\.\-_]/i', $filename); } /** * Check if a file is an SRR file. */ protected function isSrrFile(string $lowerFilename): bool { return str_ends_with($lowerFilename, '.srr') || str_ends_with($lowerFilename, '.srs'); } /** * Check if a file is a main RAR (not split). */ protected function isMainRar(string $lowerFilename): bool { return preg_match('/\.rar$/i', $lowerFilename) && ! preg_match('/\.part\d+\.rar$/i', $lowerFilename); } /** * Check if a file is the first split RAR part. */ protected function isFirstSplitRar(string $lowerFilename): bool { return (bool) preg_match('/\.part0*1\.rar$/i', $lowerFilename); } /** * Check if a file is a video file. */ protected function isVideoFile(string $lowerFilename): bool { $extension = pathinfo($lowerFilename, PATHINFO_EXTENSION); return in_array($extension, self::VIDEO_EXTENSIONS, true); } /** * Check if a file is an NFO file. */ protected function isNfoFile(string $lowerFilename): bool { return str_ends_with($lowerFilename, '.nfo'); } /** * Skip files that are usually just DVD structure noise or URL spam. */ protected function shouldIgnoreForNameMatching(string $filename): bool { $baseName = $this->extractFilenameFromPath($filename); if (preg_match('/\.url$/i', $baseName)) { return true; } $baseName = strtolower(trim((string) pathinfo($baseName, PATHINFO_FILENAME), " \t\n\r\0\x0B.-_")); foreach (self::IGNORABLE_NAME_PATTERNS as $pattern) { if (preg_match($pattern, $baseName)) { return true; } } return false; } protected function extractFilenameFromPath(string $filename): string { if (preg_match('/[\\\\\/]([^\\\\\/]+)$/', $filename, $match)) { return $match[1]; } return $filename; } /** * Extract the most likely release name from multiple RAR files. * * Analyzes multiple RAR files from a release to determine the most likely * release name by finding common patterns. * * @param array $rarFiles Array of RAR filenames * @return string|null The most likely release name or null */ public function findReleaseNameFromRarFiles(array $rarFiles): ?string { $candidates = []; foreach ($rarFiles as $file) { // Skip sample/proof files if ($this->isSampleOrProof($file)) { continue; } $extracted = $this->extractReleaseNameFromRar($file); if ($extracted !== null) { $candidates[$extracted] = ($candidates[$extracted] ?? 0) + 1; } } if (empty($candidates)) { return null; } // Return the most common candidate arsort($candidates); $topCount = reset($candidates); $topCandidates = array_filter($candidates, fn ($count) => $count === $topCount); // If multiple candidates have the same count, prefer the longest one $best = null; foreach (array_keys($topCandidates) as $candidate) { if ($best === null || strlen($candidate) > strlen($best)) { $best = $candidate; } } return $best; } /** * Extract release name from a RAR filename. */ protected function extractReleaseNameFromRar(string $filename): ?string { // Extract filename from path if (preg_match('/[\\\\\/]([^\\\\\/]+)$/', $filename, $match)) { $filename = $match[1]; } // Remove RAR extensions $baseName = preg_replace('/\.(rar|r\d{2,4}|part\d+\.rar|\d{3})$/i', '', $filename); // Check if it looks like a scene release name if (preg_match('/^([a-z0-9][a-z0-9._-]+[a-z0-9])\-([a-z0-9]{2,15})$/i', $baseName, $match)) { // Clean up any remaining artifacts $baseName = preg_replace('/[._-]?(sample|proof|subs?)$/i', '', $baseName); $baseName = trim($baseName, '.-_'); if (strlen($baseName) >= 10) { return $baseName; } } return null; } }