mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Improve categorization
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Facades\Search;
|
||||
use App\Models\Release;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use App\Services\NameFixing\NzbSplitUnwrapper;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class FixNzbSplitNames extends Command
|
||||
{
|
||||
protected $signature = 'releases:fix-nzbsplit
|
||||
{--dry-run : Preview changes without updating the database}';
|
||||
|
||||
protected $description = 'Fix NZBSPLIT-wrapped release names and recategorize the affected releases.';
|
||||
|
||||
public function handle(CategorizationService $categorizationService, NzbSplitUnwrapper $nzbSplitUnwrapper): int
|
||||
{
|
||||
$query = Release::query()
|
||||
->select(['id', 'name', 'searchname', 'fromname', 'groups_id', 'categories_id'])
|
||||
->where(function ($builder): void {
|
||||
$builder
|
||||
->where('name', 'like', '%__NZBSPLIT__%')
|
||||
->orWhere('searchname', 'like', '%__NZBSPLIT__%');
|
||||
})
|
||||
->orderBy('id');
|
||||
|
||||
$count = (clone $query)->count();
|
||||
|
||||
if ($count === 0) {
|
||||
$this->info('No NZBSPLIT-wrapped releases found.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$updated = 0;
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
$query->chunkById(250, function ($releases) use ($categorizationService, $nzbSplitUnwrapper, $dryRun, &$updated, $bar): void {
|
||||
foreach ($releases as $release) {
|
||||
$bar->advance();
|
||||
|
||||
$newTitle = $nzbSplitUnwrapper->unwrap((string) $release->searchname)
|
||||
?? $nzbSplitUnwrapper->unwrap((string) $release->name);
|
||||
|
||||
if ($newTitle === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$category = $categorizationService->determineCategory(
|
||||
$release->groups_id,
|
||||
$newTitle,
|
||||
(string) ($release->fromname ?? '')
|
||||
);
|
||||
|
||||
$newCategoryId = (int) ($category['categories_id'] ?? $release->categories_id);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->newLine();
|
||||
$this->line(sprintf(
|
||||
'[dry-run] #%d %s -> %s (%d -> %d)',
|
||||
$release->id,
|
||||
(string) $release->searchname,
|
||||
$newTitle,
|
||||
(int) $release->categories_id,
|
||||
$newCategoryId
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Release::query()
|
||||
->where('id', $release->id)
|
||||
->update([
|
||||
'name' => $newTitle,
|
||||
'searchname' => $newTitle,
|
||||
'categories_id' => $newCategoryId,
|
||||
'isrenamed' => 1,
|
||||
'iscategorized' => 1,
|
||||
'videos_id' => 0,
|
||||
'tv_episodes_id' => 0,
|
||||
'imdbid' => null,
|
||||
'musicinfo_id' => null,
|
||||
'consoleinfo_id' => null,
|
||||
'gamesinfo_id' => 0,
|
||||
'bookinfo_id' => 0,
|
||||
'anidbid' => null,
|
||||
]);
|
||||
|
||||
Search::updateRelease((int) $release->id);
|
||||
$updated++;
|
||||
}
|
||||
});
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info(sprintf('Dry run complete. %d NZBSPLIT-wrapped releases inspected.', $count));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(sprintf('Updated %d NZBSPLIT-wrapped releases.', $updated));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use App\Models\Release;
|
||||
use App\Models\ReleaseFile;
|
||||
use App\Services\AdditionalProcessing\Config\ProcessingConfiguration;
|
||||
use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext;
|
||||
use App\Services\NameFixing\FileNameCleaner;
|
||||
use App\Services\NameFixing\NameFixingService;
|
||||
use App\Services\NameFixing\ReleaseUpdateService;
|
||||
use App\Services\NfoService;
|
||||
@@ -31,13 +32,22 @@ use Illuminate\Support\Facades\Log;
|
||||
*/
|
||||
class ReleaseFileManager
|
||||
{
|
||||
private readonly ReleaseUpdateService $releaseUpdateService;
|
||||
|
||||
private readonly FileNameCleaner $fileNameCleaner;
|
||||
|
||||
public function __construct(
|
||||
private readonly ProcessingConfiguration $config,
|
||||
private readonly ReleaseImageService $releaseImage,
|
||||
private readonly NfoService $nfo,
|
||||
private readonly NzbService $nzb,
|
||||
private readonly NameFixingService $nameFixingService
|
||||
) {}
|
||||
private readonly NameFixingService $nameFixingService,
|
||||
?ReleaseUpdateService $releaseUpdateService = null,
|
||||
?FileNameCleaner $fileNameCleaner = null
|
||||
) {
|
||||
$this->releaseUpdateService = $releaseUpdateService ?? new ReleaseUpdateService;
|
||||
$this->fileNameCleaner = $fileNameCleaner ?? new FileNameCleaner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add file information to the database.
|
||||
@@ -152,6 +162,50 @@ class ReleaseFileManager
|
||||
Search::updateRelease($releaseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename NZBSPLIT-wrapped releases directly from parsed NZB titles.
|
||||
*
|
||||
* @param list<array<string, mixed>> $nzbContents
|
||||
*/
|
||||
public function processReleaseNameFromNzbContents(array $nzbContents, ReleaseProcessingContext $context): bool
|
||||
{
|
||||
foreach ($nzbContents as $nzbFile) {
|
||||
$title = (string) ($nzbFile['title'] ?? '');
|
||||
|
||||
if ($title === '' || ! str_contains($title, '__NZBSPLIT__')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidate = $this->fileNameCleaner->extractNzbSplitName($title);
|
||||
|
||||
if ($candidate === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidate = $this->fileNameCleaner->normalizeCandidateTitle($candidate);
|
||||
|
||||
if (! $this->fileNameCleaner->isPlausibleReleaseTitle($candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->releaseUpdateService->updateRelease(
|
||||
$context->release,
|
||||
$candidate,
|
||||
'NZBSPLIT wrapper',
|
||||
true,
|
||||
'Filenames, ',
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
$context->release->searchname = $candidate;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize release processing with status updates.
|
||||
*/
|
||||
@@ -519,7 +573,7 @@ class ReleaseFileManager
|
||||
$candidate = $this->normalizeCandidateTitle($candidate);
|
||||
|
||||
if ($this->isPlausibleReleaseTitle($candidate)) {
|
||||
(new ReleaseUpdateService)->updateRelease(
|
||||
$this->releaseUpdateService->updateRelease(
|
||||
$context->release,
|
||||
$candidate,
|
||||
'RarInfo FileName Match',
|
||||
@@ -545,7 +599,7 @@ class ReleaseFileManager
|
||||
$candidate = $this->normalizeCandidateTitle($candidate);
|
||||
|
||||
if ($this->isPlausibleReleaseTitle($candidate)) {
|
||||
(new ReleaseUpdateService)->updateRelease(
|
||||
$this->releaseUpdateService->updateRelease(
|
||||
$context->release,
|
||||
$candidate,
|
||||
'RarInfo FileName Match',
|
||||
@@ -566,6 +620,12 @@ class ReleaseFileManager
|
||||
private function extractReleaseNameFromFile(string $filename): ?string
|
||||
{
|
||||
$basename = basename($filename);
|
||||
|
||||
$unwrapped = $this->fileNameCleaner->extractNzbSplitName($basename);
|
||||
if ($unwrapped !== null) {
|
||||
return $unwrapped;
|
||||
}
|
||||
|
||||
$cleaned = preg_replace(
|
||||
'/\.(mkv|avi|mp4|m4v|mpg|mpeg|wmv|flv|mov|ts|vob|iso|divx|par2?|nfo|sfv|nzb|rar|zip|r\d{2,3}|pkg|exe|msi)$/i',
|
||||
'',
|
||||
@@ -588,13 +648,7 @@ class ReleaseFileManager
|
||||
*/
|
||||
private function normalizeCandidateTitle(string $title): string
|
||||
{
|
||||
$t = trim($title);
|
||||
$t = preg_replace('/\.(mkv|avi|mp4|m4v|mpg|mpeg|wmv|flv|mov|ts|vob|iso|divx)$/i', '', $t) ?? $t;
|
||||
$t = preg_replace('/\.(par2?|nfo|sfv|nzb|rar|zip|r\d{2,3}|pkg|exe|msi|jpe?g|png|gif|bmp)$/i', '', $t) ?? $t;
|
||||
$t = preg_replace('/[.\-_ ](?:part|vol|r)\d+(?:\+\d+)?$/i', '', $t) ?? $t;
|
||||
$t = preg_replace('/[\s_]+/', ' ', $t) ?? $t;
|
||||
|
||||
return trim($t, " .-_\t\r\n");
|
||||
return $this->fileNameCleaner->normalizeCandidateTitle($title);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -602,30 +656,6 @@ class ReleaseFileManager
|
||||
*/
|
||||
private function isPlausibleReleaseTitle(string $title): bool
|
||||
{
|
||||
$t = trim($title);
|
||||
if ($t === '' || strlen($t) < 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$wordCount = preg_match_all('/[A-Za-z0-9]{3,}/', $t);
|
||||
if ($wordCount < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('/(?:^|[.\-_ ])(?:part|vol|r)\d+(?:\+\d+)?$/i', $t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('/^(setup|install|installer|patch|update|crack|keygen)\d*[\s._-]/i', $t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasGroupSuffix = (bool) preg_match('/[-.][A-Za-z0-9]{2,}$/', $t);
|
||||
$hasYear = (bool) preg_match('/\b(19|20)\d{2}\b/', $t);
|
||||
$hasQuality = (bool) preg_match('/\b(480p|720p|1080p|2160p|4k|webrip|web[ .-]?dl|bluray|bdrip|dvdrip|hdtv|hdrip|xvid|x264|x265|hevc|h\.?264|ts|cam|r5|proper|repack)\b/i', $t);
|
||||
$hasTV = (bool) preg_match('/\bS\d{1,2}[Eex]\d{1,3}\b/i', $t);
|
||||
$hasXXX = (bool) preg_match('/\bXXX\b/i', $t);
|
||||
|
||||
return $hasGroupSuffix || ($hasTV && $hasQuality) || ($hasYear && ($hasQuality || $hasTV)) || $hasXXX;
|
||||
return $this->fileNameCleaner->isPlausibleReleaseTitle($title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class ReleaseProcessor
|
||||
}
|
||||
|
||||
$this->initializeContext($context);
|
||||
$this->releaseManager->processReleaseNameFromNzbContents($context->nzbContents, $context);
|
||||
$messageIds = $this->nzbParser->extractMessageIDs($context->nzbContents, $context->releaseGroupName, $this->config);
|
||||
|
||||
$context->nzbHasCompressedFile = $messageIds['hasCompressedFile'];
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Settings;
|
||||
use App\Models\UsenetGroup;
|
||||
use App\Services\Categorization\Pipes\AbstractCategorizationPipe;
|
||||
use App\Services\Categorization\Pipes\CategorizationPassable;
|
||||
use App\Services\NameFixing\NzbSplitUnwrapper;
|
||||
use Illuminate\Pipeline\Pipeline;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -31,10 +32,12 @@ class CategorizationPipeline
|
||||
|
||||
protected bool $catWebDL;
|
||||
|
||||
protected NzbSplitUnwrapper $nzbSplitUnwrapper;
|
||||
|
||||
/**
|
||||
* @param iterable<AbstractCategorizationPipe> $pipes
|
||||
*/
|
||||
public function __construct(iterable $pipes = [])
|
||||
public function __construct(iterable $pipes = [], ?NzbSplitUnwrapper $nzbSplitUnwrapper = null)
|
||||
{
|
||||
/** @phpstan-ignore argument.templateType */
|
||||
$this->pipes = collect($pipes)
|
||||
@@ -42,6 +45,7 @@ class CategorizationPipeline
|
||||
|
||||
$this->categorizeForeign = (bool) Settings::settingValue('categorizeforeign');
|
||||
$this->catWebDL = (bool) Settings::settingValue('catwebdl');
|
||||
$this->nzbSplitUnwrapper = $nzbSplitUnwrapper ?? new NzbSplitUnwrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,6 +74,8 @@ class CategorizationPipeline
|
||||
?string $poster = '',
|
||||
bool $debug = false
|
||||
): array {
|
||||
$releaseName = $this->nzbSplitUnwrapper->unwrap($releaseName) ?? $releaseName;
|
||||
|
||||
$groupName = UsenetGroup::whereId($groupId)->value('name') ?? '';
|
||||
|
||||
$context = new ReleaseContext(
|
||||
|
||||
@@ -13,6 +13,10 @@ use App\Services\Categorization\ReleaseContext;
|
||||
*/
|
||||
class TvCategorizer extends AbstractCategorizer
|
||||
{
|
||||
private const string SPORTS_MARKER_REGEX = '/\b(NFL|NBA|NHL|MLB|MLS|EPL|UFC|WWE|Boxing|F1|Formula[._ -]?1|NASCAR|PGA|Tennis|Golf|Soccer|Football|Cricket|Rugby|Olympics?|Olympic[._ -]?Games?|Paralympics?)\b/i';
|
||||
|
||||
private const string SPORTS_EVENT_CONTEXT_REGEX = '/\d{4}|\b(Season|Week|Round|Match|Game|vs|Playoffs?|Finals?|Qualifying|Opening|Closing|Ceremony|Championship)\b/i';
|
||||
|
||||
protected int $priority = 20;
|
||||
|
||||
public function getName(): string
|
||||
@@ -97,6 +101,10 @@ class TvCategorizer extends AbstractCategorizer
|
||||
if (preg_match('/[._ -](19|20)\d{2}[._ -]\d{2}[._ -]\d{2}[._ -]/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
// Sports events are TV-like even without SxxExx markers.
|
||||
if ($this->isSportEvent($name)) {
|
||||
return true;
|
||||
}
|
||||
// Known anime release groups (should be treated as TV)
|
||||
if (preg_match('/(?:^|[.\-_ \[])(URANiME|ANiHLS|HaiKU|ANiURL|SkyAnime|Erai-raws|LostYears|Vodes|SubsPlease|Judas|Ember|EMBER|YuiSubs|ASW|Tsundere-Raws|Anime-Raws|Kekkan)(?:[.\-_ \]]|$)/i', $name)) {
|
||||
return true;
|
||||
@@ -148,8 +156,7 @@ class TvCategorizer extends AbstractCategorizer
|
||||
|
||||
protected function checkSport(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\b(NFL|NBA|NHL|MLB|MLS|UFC|WWE|Boxing|F1|Formula[._ -]?1|NASCAR|PGA|Tennis|Golf|Soccer|Football|Cricket|Rugby)\b/i', $name) &&
|
||||
preg_match('/\d{4}|\b(Season|Week|Round|Match|Game)\b/i', $name)) {
|
||||
if ($this->isSportEvent($name)) {
|
||||
return $this->matched(Category::TV_SPORT, 0.85, 'sport');
|
||||
}
|
||||
|
||||
@@ -250,4 +257,10 @@ class TvCategorizer extends AbstractCategorizer
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function isSportEvent(string $name): bool
|
||||
{
|
||||
return preg_match(self::SPORTS_MARKER_REGEX, $name) === 1
|
||||
&& preg_match(self::SPORTS_EVENT_CONTEXT_REGEX, $name) === 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,16 @@ class FileNameExtractor
|
||||
|
||||
// Try each pattern in order of specificity
|
||||
|
||||
if (str_contains($filename, '__NZBSPLIT__')) {
|
||||
$unwrapped = $this->cleaner->extractNzbSplitName($filename);
|
||||
|
||||
if ($unwrapped !== null && $this->cleaner->isPlausibleReleaseTitle($unwrapped)) {
|
||||
return NameFixResult::fromMatch($unwrapped, 'NZBSPLIT wrapper', 'File');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Scene TV release with group suffix
|
||||
if (preg_match('/^(.+?(x264|x265|HEVC|XviD|H\.?264|H\.?265)\-[A-Za-z0-9]+)\\\\/i', $filename, $result)) {
|
||||
return NameFixResult::fromMatch($result[1], 'Scene release with group', 'File');
|
||||
|
||||
@@ -16,6 +16,13 @@ class FileNameCleaner
|
||||
{
|
||||
use DetectsHashedNames;
|
||||
|
||||
protected NzbSplitUnwrapper $nzbSplitUnwrapper;
|
||||
|
||||
public function __construct(?NzbSplitUnwrapper $nzbSplitUnwrapper = null)
|
||||
{
|
||||
$this->nzbSplitUnwrapper = $nzbSplitUnwrapper ?? new NzbSplitUnwrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic DVD structure filenames and similarly low-information names.
|
||||
*
|
||||
@@ -92,6 +99,8 @@ class FileNameCleaner
|
||||
// Extract filename from path
|
||||
$fileName = $this->extractFilenameFromPath($fileName);
|
||||
|
||||
$fileName = $this->extractNzbSplitName($fileName) ?? $fileName;
|
||||
|
||||
if ($this->isIgnorableForMatching($fileName)) {
|
||||
return false;
|
||||
}
|
||||
@@ -153,6 +162,8 @@ class FileNameCleaner
|
||||
{
|
||||
$t = trim($title);
|
||||
|
||||
$t = $this->extractNzbSplitName($t) ?? $t;
|
||||
|
||||
// Remove common video file extensions
|
||||
$t = preg_replace('/\.(mkv|avi|mp4|m4v|mpg|mpeg|wmv|flv|mov|ts|vob|iso|divx)$/i', '', $t) ?? $t;
|
||||
|
||||
@@ -231,6 +242,8 @@ class FileNameCleaner
|
||||
*/
|
||||
public function cleanForTitleMatch(string $filename): string
|
||||
{
|
||||
$filename = $this->extractNzbSplitName($filename) ?? $filename;
|
||||
|
||||
// Remove file extension
|
||||
$clean = preg_replace('/\.(mkv|avi|mp4|m4v|wmv|mpg|mpeg|mov|ts|m2ts|vob|divx|flv|nfo|sfv|nzb|srr|srs|rar|r\d{2,4}|zip|7z|par2?|vol\d+[\+\-]\d+|\d{3})$/i', '', $filename);
|
||||
|
||||
@@ -271,6 +284,8 @@ class FileNameCleaner
|
||||
*/
|
||||
public function looksLikeSceneRelease(string $filename): bool
|
||||
{
|
||||
$filename = $this->extractNzbSplitName($filename) ?? $filename;
|
||||
|
||||
$baseName = preg_replace('/\.[a-z0-9]{2,4}$/i', '', $filename);
|
||||
|
||||
// Check for group suffix
|
||||
@@ -313,6 +328,11 @@ class FileNameCleaner
|
||||
return false;
|
||||
}
|
||||
|
||||
public function extractNzbSplitName(string $value): ?string
|
||||
{
|
||||
return $this->nzbSplitUnwrapper->unwrap($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore junk inputs that should never drive PreDB matches.
|
||||
*/
|
||||
|
||||
@@ -1200,6 +1200,13 @@ class NameFixingService
|
||||
break;
|
||||
|
||||
case 'Filenames, ':
|
||||
// Try direct file name extraction first (handles NZBSPLIT wrappers)
|
||||
if (! $this->updateService->matched) {
|
||||
$result = $this->fileExtractor->extractFromFile($release->textstring);
|
||||
if ($result !== null) {
|
||||
$this->updateService->updateRelease($release, $result->newName, 'fileCheck: '.$result->method, $echo, $type, $nameStatus, $show);
|
||||
}
|
||||
}
|
||||
// Try PreDB file check
|
||||
if (! $this->updateService->matched) {
|
||||
$this->preDbFileCheck($release, $echo, $type, $nameStatus, $show);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\NameFixing;
|
||||
|
||||
final class NzbSplitUnwrapper
|
||||
{
|
||||
private const string WRAPPER_PATTERN = '/__NZBSPLIT__([a-f0-9]{8,})__NZBSPLIT__(.+)$/i';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private const array TRAILING_ARCHIVE_PATTERNS = [
|
||||
'/\.part\d+\.rar$/i',
|
||||
'/\.r\d{2,4}$/i',
|
||||
'/\.7z\.\d{2,4}$/i',
|
||||
'/\.7z$/i',
|
||||
'/\.rar$/i',
|
||||
'/\.par2?$/i',
|
||||
'/\.vol\d+[+\-]\d+\.par2?$/i',
|
||||
'/\.\d{3}$/',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private const array TRAILING_SUBJECT_PATTERNS = [
|
||||
'/"\s*yEnc(?:\s*\([^)]*\))?$/i',
|
||||
'/\s+yEnc(?:\s*\([^)]*\))?$/i',
|
||||
'/"\s*yEnc.*$/i',
|
||||
'/\s+yEnc.*$/i',
|
||||
];
|
||||
|
||||
public function unwrap(string $value): ?string
|
||||
{
|
||||
if (! preg_match(self::WRAPPER_PATTERN, $value, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$title = $matches[2];
|
||||
$didStrip = true;
|
||||
|
||||
while ($didStrip) {
|
||||
$didStrip = false;
|
||||
|
||||
foreach (self::TRAILING_SUBJECT_PATTERNS as $pattern) {
|
||||
$updated = preg_replace($pattern, '', $title);
|
||||
|
||||
if ($updated !== null && $updated !== $title) {
|
||||
$title = rtrim($updated, " \t\n\r\0\x0B\"'");
|
||||
$didStrip = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (self::TRAILING_ARCHIVE_PATTERNS as $pattern) {
|
||||
$updated = preg_replace($pattern, '', $title);
|
||||
|
||||
if ($updated !== null && $updated !== $title) {
|
||||
$title = $updated;
|
||||
$didStrip = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$title = str_replace(['_', '+'], ['.', ' '], $title);
|
||||
$title = preg_replace('/\.{2,}/', '.', $title) ?? $title;
|
||||
$title = preg_replace('/\s{2,}/', ' ', $title) ?? $title;
|
||||
$title = preg_replace('/\.\s+/', '.', $title) ?? $title;
|
||||
$title = preg_replace('/\s+\./', '.', $title) ?? $title;
|
||||
$title = trim($title, " \t\n\r\0\x0B.-_");
|
||||
|
||||
return $title !== '' ? $title : null;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Predb;
|
||||
use App\Services\NameFixing\NzbSplitUnwrapper;
|
||||
|
||||
/**
|
||||
* Cleans names for releases/imports/namefixer.
|
||||
@@ -48,6 +49,8 @@ class ReleaseCleaningService
|
||||
|
||||
protected RegexService $_regexes;
|
||||
|
||||
protected NzbSplitUnwrapper $nzbSplitUnwrapper;
|
||||
|
||||
/**
|
||||
* ReleaseCleaningService constructor.
|
||||
*
|
||||
@@ -60,6 +63,7 @@ class ReleaseCleaningService
|
||||
$this->e1 = CollectionsCleaningService::REGEX_FILE_EXTENSIONS.CollectionsCleaningService::REGEX_END;
|
||||
$this->e2 = CollectionsCleaningService::REGEX_FILE_EXTENSIONS.CollectionsCleaningService::REGEX_SUBJECT_SIZE.CollectionsCleaningService::REGEX_END;
|
||||
$this->_regexes = new RegexService('release_naming_regexes');
|
||||
$this->nzbSplitUnwrapper = new NzbSplitUnwrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +77,19 @@ class ReleaseCleaningService
|
||||
$this->fromName = $fromName;
|
||||
$this->groupName = $groupName;
|
||||
|
||||
$unwrappedSubject = $this->nzbSplitUnwrapper->unwrap($subject);
|
||||
if ($unwrappedSubject !== null) {
|
||||
$this->subject = $unwrappedSubject;
|
||||
|
||||
return [
|
||||
'cleansubject' => $unwrappedSubject,
|
||||
'properlynamed' => true,
|
||||
'increment' => false,
|
||||
'predb' => 0,
|
||||
'requestid' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$hit = $hits = [];
|
||||
// Get pre style name from releases.name
|
||||
if (preg_match_all(
|
||||
@@ -429,7 +446,7 @@ class ReleaseCleaningService
|
||||
*/
|
||||
public function fixerCleaner(string $name)
|
||||
{
|
||||
$cleanerName = $name;
|
||||
$cleanerName = $this->nzbSplitUnwrapper->unwrap($name) ?? $name;
|
||||
|
||||
// Remove sample/proof/thumbs markers from the end
|
||||
$cleanerName = preg_replace('/[.\-_](sample|proof|thumbs?)$/i', '', $cleanerName);
|
||||
|
||||
Generated
+38
-38
@@ -1934,16 +1934,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v13.5.0",
|
||||
"version": "v13.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "ffa1850049a691b93129808f27ecd10e65c9d1a5"
|
||||
"reference": "416a93ea9c53161e0d4b8a44045f447b65a7d2f1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/ffa1850049a691b93129808f27ecd10e65c9d1a5",
|
||||
"reference": "ffa1850049a691b93129808f27ecd10e65c9d1a5",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/416a93ea9c53161e0d4b8a44045f447b65a7d2f1",
|
||||
"reference": "416a93ea9c53161e0d4b8a44045f447b65a7d2f1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2153,20 +2153,20 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2026-04-14T13:55:03+00:00"
|
||||
"time": "2026-04-21T13:32:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/horizon",
|
||||
"version": "v5.45.6",
|
||||
"version": "v5.46.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/horizon.git",
|
||||
"reference": "629707ff3cce9ff72715e7815f74dda10bdcbe0a"
|
||||
"reference": "bfea968e8aa674fb649d02e55ea0d38bdf5137d5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/horizon/zipball/629707ff3cce9ff72715e7815f74dda10bdcbe0a",
|
||||
"reference": "629707ff3cce9ff72715e7815f74dda10bdcbe0a",
|
||||
"url": "https://api.github.com/repos/laravel/horizon/zipball/bfea968e8aa674fb649d02e55ea0d38bdf5137d5",
|
||||
"reference": "bfea968e8aa674fb649d02e55ea0d38bdf5137d5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2231,9 +2231,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/horizon/issues",
|
||||
"source": "https://github.com/laravel/horizon/tree/v5.45.6"
|
||||
"source": "https://github.com/laravel/horizon/tree/v5.46.0"
|
||||
},
|
||||
"time": "2026-04-14T13:31:53+00:00"
|
||||
"time": "2026-04-20T18:08:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
@@ -11848,16 +11848,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/boost",
|
||||
"version": "v2.4.4",
|
||||
"version": "v2.4.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/boost.git",
|
||||
"reference": "db101b977897e00c6d2e40e9b610591cb0aa277e"
|
||||
"reference": "60386c7723ff7cb388b62b6c137597244a9cf2f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/boost/zipball/db101b977897e00c6d2e40e9b610591cb0aa277e",
|
||||
"reference": "db101b977897e00c6d2e40e9b610591cb0aa277e",
|
||||
"url": "https://api.github.com/repos/laravel/boost/zipball/60386c7723ff7cb388b62b6c137597244a9cf2f2",
|
||||
"reference": "60386c7723ff7cb388b62b6c137597244a9cf2f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11866,7 +11866,7 @@
|
||||
"illuminate/contracts": "^11.45.3|^12.41.1|^13.0",
|
||||
"illuminate/routing": "^11.45.3|^12.41.1|^13.0",
|
||||
"illuminate/support": "^11.45.3|^12.41.1|^13.0",
|
||||
"laravel/mcp": "^0.5.1|^0.6.0",
|
||||
"laravel/mcp": "^0.5.1|^0.6.0|^0.7.0",
|
||||
"laravel/prompts": "^0.3.10",
|
||||
"laravel/roster": "^0.5.0",
|
||||
"php": "^8.2"
|
||||
@@ -11910,20 +11910,20 @@
|
||||
"issues": "https://github.com/laravel/boost/issues",
|
||||
"source": "https://github.com/laravel/boost"
|
||||
},
|
||||
"time": "2026-04-16T16:53:05+00:00"
|
||||
"time": "2026-04-22T13:29:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/mcp",
|
||||
"version": "v0.6.7",
|
||||
"version": "v0.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/mcp.git",
|
||||
"reference": "c3775e57b95d7eadb580d543689d9971ec8721f2"
|
||||
"reference": "3513b4feca5f1678be4d2261dcfa8e456436d02a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/mcp/zipball/c3775e57b95d7eadb580d543689d9971ec8721f2",
|
||||
"reference": "c3775e57b95d7eadb580d543689d9971ec8721f2",
|
||||
"url": "https://api.github.com/repos/laravel/mcp/zipball/3513b4feca5f1678be4d2261dcfa8e456436d02a",
|
||||
"reference": "3513b4feca5f1678be4d2261dcfa8e456436d02a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11983,7 +11983,7 @@
|
||||
"issues": "https://github.com/laravel/mcp/issues",
|
||||
"source": "https://github.com/laravel/mcp"
|
||||
},
|
||||
"time": "2026-04-15T08:30:42+00:00"
|
||||
"time": "2026-04-21T10:23:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
@@ -12450,23 +12450,23 @@
|
||||
},
|
||||
{
|
||||
"name": "nunomaduro/collision",
|
||||
"version": "v8.9.3",
|
||||
"version": "v8.9.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nunomaduro/collision.git",
|
||||
"reference": "b0d8ab95b29c3189aeeb902d81215231df4c1b64"
|
||||
"reference": "716af8f95a470e9094cfca09ed897b023be191a5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/b0d8ab95b29c3189aeeb902d81215231df4c1b64",
|
||||
"reference": "b0d8ab95b29c3189aeeb902d81215231df4c1b64",
|
||||
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5",
|
||||
"reference": "716af8f95a470e9094cfca09ed897b023be191a5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"filp/whoops": "^2.18.4",
|
||||
"nunomaduro/termwind": "^2.4.0",
|
||||
"php": "^8.2.0",
|
||||
"symfony/console": "^7.4.8 || ^8.0.4"
|
||||
"symfony/console": "^7.4.8 || ^8.0.8"
|
||||
},
|
||||
"conflict": {
|
||||
"laravel/framework": "<11.48.0 || >=14.0.0",
|
||||
@@ -12474,12 +12474,12 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.8.5",
|
||||
"larastan/larastan": "^3.9.3",
|
||||
"laravel/framework": "^11.48.0 || ^12.56.0 || ^13.2.0",
|
||||
"laravel/pint": "^1.29.0",
|
||||
"orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.0.0",
|
||||
"larastan/larastan": "^3.9.6",
|
||||
"laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0",
|
||||
"laravel/pint": "^1.29.1",
|
||||
"orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1",
|
||||
"pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0",
|
||||
"sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.0.0"
|
||||
"sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -12542,7 +12542,7 @@
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-06T19:25:53+00:00"
|
||||
"time": "2026-04-21T14:04:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phar-io/manifest",
|
||||
@@ -12664,11 +12664,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.50",
|
||||
"version": "2.1.51",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d452086fb4cf648c6b2d8cf3b639351f79e4f3e2",
|
||||
"reference": "d452086fb4cf648c6b2d8cf3b639351f79e4f3e2",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59",
|
||||
"reference": "dc3b523c45e714c70de2ac5113b958223b55dc59",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12713,7 +12713,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-17T13:10:32+00:00"
|
||||
"time": "2026-04-21T18:22:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -14953,5 +14953,5 @@
|
||||
"ext-zlib": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use App\Services\AdditionalProcessing\Config\ProcessingConfiguration;
|
||||
use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext;
|
||||
use App\Services\AdditionalProcessing\ReleaseFileManager;
|
||||
use App\Services\NameFixing\FileNameCleaner;
|
||||
use App\Services\NameFixing\NameFixingService;
|
||||
use App\Services\NameFixing\ReleaseUpdateService;
|
||||
use App\Services\NfoService;
|
||||
use App\Services\Nzb\NzbService;
|
||||
use App\Services\ReleaseImageService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AdditionalProcessingNzbSplitRenameTest extends TestCase
|
||||
{
|
||||
public function test_additional_postprocessing_renames_nzb_split_release_from_nzb_titles(): void
|
||||
{
|
||||
$wrapped = 'WinterOlympics2026__NZBSPLIT__0456f274737cea074abd86a89144cc7b__NZBSPLIT__Winter_Olympic_Games_Milano_Cortina_2026_Closing_Ceremony_1080p25_WEB-DL_(MultiAudio).7z.065" yEnc(1';
|
||||
$expected = 'Winter.Olympic.Games.Milano.Cortina.2026.Closing.Ceremony.1080p25.WEB-DL.(MultiAudio)';
|
||||
|
||||
$release = new Release([
|
||||
'id' => 123,
|
||||
'releases_id' => 123,
|
||||
'name' => $wrapped,
|
||||
'searchname' => $wrapped,
|
||||
'fromname' => 'poster@example.com',
|
||||
'guid' => str_repeat('a', 40),
|
||||
'groups_id' => 1,
|
||||
'categories_id' => Category::OTHER_HASHED,
|
||||
]);
|
||||
|
||||
$context = new ReleaseProcessingContext($release);
|
||||
$updateService = $this->createMock(ReleaseUpdateService::class);
|
||||
$updateService->expects($this->once())
|
||||
->method('updateRelease')
|
||||
->with(
|
||||
$release,
|
||||
$expected,
|
||||
'NZBSPLIT wrapper',
|
||||
true,
|
||||
'Filenames, ',
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
$manager = $this->makeReleaseFileManager($updateService);
|
||||
|
||||
$renamed = $manager->processReleaseNameFromNzbContents([
|
||||
['title' => $wrapped],
|
||||
], $context);
|
||||
|
||||
$this->assertTrue($renamed);
|
||||
$this->assertSame($expected, $context->release->searchname);
|
||||
}
|
||||
|
||||
public function test_additional_postprocessing_skips_low_information_nzb_split_payloads(): void
|
||||
{
|
||||
$wrapped = 'TEST__NZBSPLIT__1234567890abcdef__NZBSPLIT__setup.7z.001';
|
||||
$release = new Release([
|
||||
'id' => 124,
|
||||
'releases_id' => 124,
|
||||
'name' => $wrapped,
|
||||
'searchname' => $wrapped,
|
||||
'groups_id' => 1,
|
||||
'categories_id' => Category::OTHER_HASHED,
|
||||
]);
|
||||
|
||||
$context = new ReleaseProcessingContext($release);
|
||||
$updateService = $this->createMock(ReleaseUpdateService::class);
|
||||
$updateService->expects($this->never())->method('updateRelease');
|
||||
|
||||
$manager = $this->makeReleaseFileManager($updateService);
|
||||
|
||||
$renamed = $manager->processReleaseNameFromNzbContents([
|
||||
['title' => $wrapped],
|
||||
], $context);
|
||||
|
||||
$this->assertFalse($renamed);
|
||||
$this->assertSame($wrapped, $context->release->searchname);
|
||||
}
|
||||
|
||||
public function test_archive_name_extractor_unwraps_nzb_split_file_names(): void
|
||||
{
|
||||
$manager = $this->makeReleaseFileManager();
|
||||
$extractMethod = new \ReflectionMethod($manager, 'extractReleaseNameFromFile');
|
||||
|
||||
$result = $extractMethod->invoke(
|
||||
$manager,
|
||||
'MLB__NZBSPLIT__f5054661a1d468cc6a8712de8217c1c9__NZBSPLIT__MLB_2026_Cincinnati_Reds_vs_Tampa_Bay_Rays_20_04_720pEN60fps.7z.075'
|
||||
);
|
||||
|
||||
$this->assertSame('MLB.2026.Cincinnati.Reds.vs.Tampa.Bay.Rays.20.04.720pEN60fps', $result);
|
||||
}
|
||||
|
||||
private function makeReleaseFileManager(?ReleaseUpdateService $updateService = null): ReleaseFileManager
|
||||
{
|
||||
$updateService ??= $this->createMock(ReleaseUpdateService::class);
|
||||
|
||||
return new ReleaseFileManager(
|
||||
$this->makeConfiguration(),
|
||||
$this->createMock(ReleaseImageService::class),
|
||||
$this->createMock(NfoService::class),
|
||||
$this->createMock(NzbService::class),
|
||||
$this->createMock(NameFixingService::class),
|
||||
$updateService,
|
||||
new FileNameCleaner
|
||||
);
|
||||
}
|
||||
|
||||
private function makeConfiguration(): ProcessingConfiguration
|
||||
{
|
||||
$reflection = new \ReflectionClass(ProcessingConfiguration::class);
|
||||
|
||||
/** @var ProcessingConfiguration $config */
|
||||
$config = $reflection->newInstanceWithoutConstructor();
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,54 @@ class ReleaseNameFixedRecategorizationTest extends TestCase
|
||||
$this->assertSame(1, (int) $release->isrenamed);
|
||||
}
|
||||
|
||||
public function test_renaming_olympic_webdl_release_recategorizes_it_from_movie_webdl_to_tv_sport(): void
|
||||
{
|
||||
Search::shouldReceive('updateRelease')->twice();
|
||||
|
||||
$group = UsenetGroup::query()->create([
|
||||
'name' => 'alt.binaries.hdtv',
|
||||
'active' => 1,
|
||||
'backfill' => 0,
|
||||
]);
|
||||
|
||||
$oldName = 'WinterOlympics2026__NZBSPLIT__0456f274737cea074abd86a89144cc7b__NZBSPLIT__Winter_Olympic_Games_Milano_Cortina_2026_Closing_Ceremony_1080p25_WEB-DL_(MultiAudio).7z.065';
|
||||
$newName = 'Winter.Olympic.Games.Milano.Cortina.2026.Closing.Ceremony.1080p25.WEB-DL.(MultiAudio)';
|
||||
|
||||
$release = Release::factory()->create([
|
||||
'name' => $oldName,
|
||||
'searchname' => $oldName,
|
||||
'fromname' => 'poster@example.com',
|
||||
'groups_id' => $group->id,
|
||||
'categories_id' => Category::MOVIE_WEBDL,
|
||||
'iscategorized' => 1,
|
||||
'isrenamed' => 0,
|
||||
'guid' => str_repeat('b', 40),
|
||||
'leftguid' => 'b',
|
||||
'nzb_guid' => 'test-olympics',
|
||||
'size' => 1,
|
||||
'postdate' => now(),
|
||||
'adddate' => now(),
|
||||
]);
|
||||
|
||||
$service = app(ReleaseUpdateService::class);
|
||||
$service->updateRelease(
|
||||
$release->fresh(),
|
||||
$newName,
|
||||
'NZBSPLIT wrapper',
|
||||
true,
|
||||
'Filenames, ',
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
$release->refresh();
|
||||
|
||||
$this->assertSame($newName, $release->searchname);
|
||||
$this->assertSame(Category::TV_SPORT, $release->categories_id);
|
||||
$this->assertSame(1, (int) $release->iscategorized);
|
||||
$this->assertSame(1, (int) $release->isrenamed);
|
||||
}
|
||||
|
||||
private function setEnvironmentValue(string $key, ?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\Categorizers\TvCategorizer;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
use App\Services\NameFixing\NzbSplitUnwrapper;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CategorizeSportTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return array<string, array{0: string, 1: string}>
|
||||
*/
|
||||
public static function nzbSplitSportProvider(): array
|
||||
{
|
||||
return [
|
||||
'mlb reds rays' => [
|
||||
'MLB__NZBSPLIT__f5054661a1d468cc6a8712de8217c1c9__NZBSPLIT__MLB_2026_Cincinnati_Reds_vs_Tampa_Bay_Rays_20_04_720pEN60fps.7z.075',
|
||||
'MLB.2026.Cincinnati.Reds.vs.Tampa.Bay.Rays.20.04.720pEN60fps',
|
||||
],
|
||||
'winter olympics closing ceremony' => [
|
||||
'WinterOlympics2026__NZBSPLIT__0456f274737cea074abd86a89144cc7b__NZBSPLIT__Winter_Olympic_Games_Milano_Cortina_2026_Closing_Ceremony_1080p25_WEB-DL_(MultiAudio).7z.065',
|
||||
'Winter.Olympic.Games.Milano.Cortina.2026.Closing.Ceremony.1080p25.WEB-DL.(MultiAudio)',
|
||||
],
|
||||
'nba playoffs wolves nuggets' => [
|
||||
'NBA__NZBSPLIT__9b1e889d7fb8d8a63c21217266069c71__NZBSPLIT__NBA_2026_Playoffs_Minnesota_Timberwolves_vs_Denver_Nuggets_Game_2_20_04_1080pEN60fps_NBC.7z.074',
|
||||
'NBA.2026.Playoffs.Minnesota.Timberwolves.vs.Denver.Nuggets.Game.2.20.04.1080pEN60fps.NBC',
|
||||
],
|
||||
'nba abc feed' => [
|
||||
'NBA__NZBSPLIT__bdab31d6f79989608009e7e8eadcbe66__NZBSPLIT__NBA_20260419_PHI_BOS_1080p60_ABC.7z.073',
|
||||
'NBA.20260419.PHI.BOS.1080p60.ABC',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('nzbSplitSportProvider')]
|
||||
public function test_unwrapped_nzb_split_sports_are_classified_as_tv_sport(string $wrapped, string $expectedName): void
|
||||
{
|
||||
$unwrapper = new NzbSplitUnwrapper;
|
||||
$categorizer = new TvCategorizer;
|
||||
|
||||
$unwrapped = $unwrapper->unwrap($wrapped);
|
||||
|
||||
$this->assertSame($expectedName, $unwrapped);
|
||||
|
||||
$context = new ReleaseContext(
|
||||
releaseName: $unwrapped ?? '',
|
||||
groupId: 0,
|
||||
groupName: '',
|
||||
poster: ''
|
||||
);
|
||||
|
||||
$result = $categorizer->categorize($context);
|
||||
|
||||
$this->assertTrue($result->isSuccessful(), "Expected TV sport match for: {$wrapped}");
|
||||
$this->assertSame(Category::TV_SPORT, $result->categoryId, "Expected TV_SPORT for: {$wrapped}");
|
||||
$this->assertNotSame(Category::MOVIE_HD, $result->categoryId);
|
||||
}
|
||||
|
||||
public function test_plain_epl_release_is_classified_as_tv_sport(): void
|
||||
{
|
||||
$categorizer = new TvCategorizer;
|
||||
|
||||
$context = new ReleaseContext(
|
||||
releaseName: 'EPL.2026.Tottenham.vs.Brighton.18.04.720pEN60fps.Fubo',
|
||||
groupId: 0,
|
||||
groupName: '',
|
||||
poster: ''
|
||||
);
|
||||
|
||||
$result = $categorizer->categorize($context);
|
||||
|
||||
$this->assertTrue($result->isSuccessful());
|
||||
$this->assertSame(Category::TV_SPORT, $result->categoryId);
|
||||
$this->assertNotSame(Category::MOVIE_HD, $result->categoryId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\NameFixing\Extractors\FileNameExtractor;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class FileNameExtractorTest extends TestCase
|
||||
{
|
||||
public function test_extracts_release_name_from_nzb_split_wrapper(): void
|
||||
{
|
||||
$extractor = new FileNameExtractor;
|
||||
|
||||
$result = $extractor->extractFromFile(
|
||||
'NBA__NZBSPLIT__bdab31d6f79989608009e7e8eadcbe66__NZBSPLIT__NBA_20260419_PHI_BOS_1080p60_ABC.7z.073'
|
||||
);
|
||||
|
||||
$this->assertNotNull($result);
|
||||
$this->assertSame('NBA.20260419.PHI.BOS.1080p60.ABC', $result->newName);
|
||||
$this->assertSame('NZBSPLIT wrapper', $result->method);
|
||||
$this->assertSame('File', $result->checkerName);
|
||||
}
|
||||
|
||||
public function test_rejects_low_information_nzb_split_payloads(): void
|
||||
{
|
||||
$extractor = new FileNameExtractor;
|
||||
|
||||
$result = $extractor->extractFromFile(
|
||||
'TEST__NZBSPLIT__1234567890abcdef__NZBSPLIT__setup.7z.001'
|
||||
);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\NameFixing\NzbSplitUnwrapper;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class NzbSplitUnwrapperTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return array<string, array{0: string, 1: string}>
|
||||
*/
|
||||
public static function wrappedReleaseProvider(): array
|
||||
{
|
||||
return [
|
||||
'winter olympics closing ceremony' => [
|
||||
'WinterOlympics2026__NZBSPLIT__0456f274737cea074abd86a89144cc7b__NZBSPLIT__Winter_Olympic_Games_Milano_Cortina_2026_Closing_Ceremony_1080p25_WEB-DL_(MultiAudio).7z.065',
|
||||
'Winter.Olympic.Games.Milano.Cortina.2026.Closing.Ceremony.1080p25.WEB-DL.(MultiAudio)',
|
||||
],
|
||||
'winter olympics with yenc tail' => [
|
||||
'WinterOlympics2026__NZBSPLIT__0456f274737cea074abd86a89144cc7b__NZBSPLIT__Winter_Olympic_Games_Milano_Cortina_2026_Closing_Ceremony_1080p25_WEB-DL_(MultiAudio).7z.065" yEnc(1',
|
||||
'Winter.Olympic.Games.Milano.Cortina.2026.Closing.Ceremony.1080p25.WEB-DL.(MultiAudio)',
|
||||
],
|
||||
'mlb reds rays' => [
|
||||
'MLB__NZBSPLIT__f5054661a1d468cc6a8712de8217c1c9__NZBSPLIT__MLB_2026_Cincinnati_Reds_vs_Tampa_Bay_Rays_20_04_720pEN60fps.7z.075',
|
||||
'MLB.2026.Cincinnati.Reds.vs.Tampa.Bay.Rays.20.04.720pEN60fps',
|
||||
],
|
||||
'nba playoffs wolves nuggets' => [
|
||||
'NBA__NZBSPLIT__9b1e889d7fb8d8a63c21217266069c71__NZBSPLIT__NBA_2026_Playoffs_Minnesota_Timberwolves_vs_Denver_Nuggets_Game_2_20_04_1080pEN60fps_NBC.7z.074',
|
||||
'NBA.2026.Playoffs.Minnesota.Timberwolves.vs.Denver.Nuggets.Game.2.20.04.1080pEN60fps.NBC',
|
||||
],
|
||||
'nba abc feed' => [
|
||||
'NBA__NZBSPLIT__bdab31d6f79989608009e7e8eadcbe66__NZBSPLIT__NBA_20260419_PHI_BOS_1080p60_ABC.7z.073',
|
||||
'NBA.20260419.PHI.BOS.1080p60.ABC',
|
||||
],
|
||||
'mlb blue jays diamondbacks' => [
|
||||
'MLB__NZBSPLIT__bc07883ca7b3c29e5cca57b8ebef3ef7__NZBSPLIT__MLB_2026_Toronto_Maple_Leafs_vs_Arizona_Diamondbacks_19_04_720pEN60fps_SN1.7z.028',
|
||||
'MLB.2026.Toronto.Maple.Leafs.vs.Arizona.Diamondbacks.19.04.720pEN60fps.SN1',
|
||||
],
|
||||
'mlb padres dodgers' => [
|
||||
'MLB__NZBSPLIT__9a0239e72a5201f889929a81eac53d9a__NZBSPLIT__MLB_2026_San_Diego_Padres_vs_Los_Angeles_Angels_19_04_720pEN60fps.7z.057',
|
||||
'MLB.2026.San.Diego.Padres.vs.Los.Angeles.Angels.19.04.720pEN60fps',
|
||||
],
|
||||
'multipart rar' => [
|
||||
'TV__NZBSPLIT__1234567890abcdef__NZBSPLIT__Some_Show_2026_Episode_01_1080p_WEB-DL.part01.rar',
|
||||
'Some.Show.2026.Episode.01.1080p.WEB-DL',
|
||||
],
|
||||
'scene rxx archive' => [
|
||||
'MOVIE__NZBSPLIT__1234567890abcdef__NZBSPLIT__Some_Movie_2026_1080p_WEB-DL-GROUP.r05',
|
||||
'Some.Movie.2026.1080p.WEB-DL-GROUP',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: string}>
|
||||
*/
|
||||
public static function invalidWrappedReleaseProvider(): array
|
||||
{
|
||||
return [
|
||||
'missing marker' => ['Regular.Release.Name.2026.1080p.WEB-DL'],
|
||||
'non hex hash' => ['NBA__NZBSPLIT__not-a-hex-hash__NZBSPLIT__NBA_2026_Finals_Game_1_1080p.7z.001'],
|
||||
'short hash' => ['NBA__NZBSPLIT__abc1234__NZBSPLIT__NBA_2026_Finals_Game_1_1080p.7z.001'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('wrappedReleaseProvider')]
|
||||
public function test_unwrap_returns_embedded_release_name(string $wrapped, string $expected): void
|
||||
{
|
||||
$unwrapper = new NzbSplitUnwrapper;
|
||||
|
||||
$this->assertSame($expected, $unwrapper->unwrap($wrapped));
|
||||
}
|
||||
|
||||
#[DataProvider('invalidWrappedReleaseProvider')]
|
||||
public function test_unwrap_returns_null_for_invalid_or_non_wrapped_input(string $wrapped): void
|
||||
{
|
||||
$unwrapper = new NzbSplitUnwrapper;
|
||||
|
||||
$this->assertNull($unwrapper->unwrap($wrapped));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user