Files
newznab-tmux-NNTmux/app/Services/NameFixing/FilePrioritizer.php
T
2026-03-11 15:36:48 +01:00

341 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\NameFixing;
/**
* Service for prioritizing files during name matching.
*
* Handles file prioritization logic for PreDB matching and name extraction.
*/
class FilePrioritizer
{
/**
* Video file extensions.
*/
private const VIDEO_EXTENSIONS = ['mkv', 'avi', 'mp4', 'm4v', 'wmv', 'divx', 'ts', 'm2ts'];
/**
* Priority levels (lower = higher priority).
*/
/** @phpstan-ignore classConstant.unused */
private const PRIORITY_SRR = 1;
private const PRIORITY_MAIN_RAR = 2;
private const PRIORITY_FIRST_PART_RAR = 3;
private const PRIORITY_VIDEO = 4;
private const PRIORITY_NFO = 5;
private const PRIORITY_OTHER_RAR = 6;
private const PRIORITY_SAMPLE = 100;
private const PRIORITY_OTHER = 50;
/**
* Generic DVD structure names and similar low-information files.
*
* @var list<string>
*/
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<string, mixed> $files Array of filenames
* @return array<string, mixed> 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<string, mixed> $files Array of filenames
* @return array<string, mixed> 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<string, mixed> $files Array of filenames
* @return array<string, mixed> 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<string, mixed> $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;
}
}