Fix tv seasons categorization

This commit is contained in:
DariusIII
2026-08-11 12:35:48 +02:00
parent 45c766d217
commit c85ee543c5
5 changed files with 177 additions and 6 deletions
@@ -27,6 +27,12 @@ class MovieCategorizer extends AbstractCategorizer
return true;
}
// A standalone Sxx token identifies a full TV season even when year or
// edition markers appear before the source and quality markers.
if ($context->hasStandaloneSeasonToken()) {
return true;
}
// Skip if it looks like a TV episode (S01E01) or season pack (S01.1080p)
if (preg_match('/[._ -]S\d{1,3}[._ -]?(E\d|D\d|Complete|Full|1080|720|480|2160|WEB|HDTV|BluRay|NF|AMZN)/i', $context->releaseName)) {
return true;
@@ -46,7 +46,7 @@ class TvCategorizer extends AbstractCategorizer
return $result;
}
if (! $this->looksLikeTV($name)) {
if (! $this->looksLikeTV($context)) {
return $this->noMatch();
}
@@ -77,15 +77,22 @@ class TvCategorizer extends AbstractCategorizer
if ($result = $this->checkSD($name)) {
return $result;
}
if ($result = $this->checkOther($name)) {
if ($result = $this->checkOther($context)) {
return $result;
}
return $this->noMatch();
}
protected function looksLikeTV(string $name): bool
protected function looksLikeTV(ReleaseContext $context): bool
{
$name = $context->releaseName;
// Standalone season token: S01, S02, etc. Quality markers may appear later.
if ($context->hasStandaloneSeasonToken()) {
return true;
}
// Season + Episode pattern: S01E01, S01.E01, S1D1, etc.
if (preg_match('/[._ -]s\d{1,3}[._ -]?(e|d(isc)?)\d{1,3}([._ -]|$)/i', $name)) {
return true;
@@ -284,14 +291,16 @@ class TvCategorizer extends AbstractCategorizer
return null;
}
protected function checkOther(string $name): ?CategorizationResult
protected function checkOther(ReleaseContext $context): ?CategorizationResult
{
$name = $context->releaseName;
// Season + episode pattern
if (preg_match('/[._ -]s\d{1,3}[._ -]?(e|d(isc)?)\d{1,3}([._ -]|$)/i', $name)) {
return $this->matched(Category::TV_OTHER, 0.6, 'tv_other');
}
// Season pack pattern (S01, S02, etc.) with any quality marker
if (preg_match('/[._ -]S\d{1,3}[._ -]/i', $name)) {
// Standalone season pack pattern (S01, S02, etc.)
if ($context->hasStandaloneSeasonToken()) {
return $this->matched(Category::TV_OTHER, 0.6, 'tv_season_pack');
}
@@ -9,6 +9,8 @@ namespace App\Services\Categorization;
*/
class ReleaseContext
{
private const string STANDALONE_SEASON_TOKEN_REGEX = '/(?:^|[._ -])S\d{1,3}(?=$|[._ -])/i';
public function __construct(
public readonly string $releaseName,
public readonly int|string $groupId,
@@ -50,6 +52,14 @@ class ReleaseContext
return (bool) preg_match($pattern, $this->groupName);
}
/**
* Check whether the release name contains a delimiter-bounded season-pack token.
*/
public function hasStandaloneSeasonToken(): bool
{
return preg_match(self::STANDALONE_SEASON_TOKEN_REGEX, $this->releaseName) === 1;
}
/**
* Check if this release has adult/XXX markers.
*/
@@ -130,6 +130,53 @@ class ReleaseNameFixedRecategorizationTest extends TestCase
$this->assertSame(1, (int) $release->isrenamed);
}
public function test_renaming_tv_episode_to_full_season_keeps_it_out_of_movies(): void
{
Search::shouldReceive('updateRelease')->twice();
$group = UsenetGroup::query()->create([
'name' => 'alt.binaries.warcraft',
'active' => 1,
'backfill' => 0,
]);
$oldName = 'Tale.of.the.Nine.Tailed.S02E10.2023.1080p.AMZN.WEB-DL.x264.DDP2.0-ADWeb';
$newName = 'Tale.of.the.Nine.Tailed.S02.2023.1080p.AMZN.WEB-DL.x264.DDP2.0-ADWeb';
$release = Release::factory()->create([
'name' => '[1/8] - "'.$oldName.'.par2" yEnc',
'searchname' => $oldName,
'fromname' => 'poster@example.com',
'groups_id' => $group->id,
'categories_id' => Category::TV_WEBDL,
'iscategorized' => 1,
'isrenamed' => 0,
'guid' => str_repeat('e', 40),
'leftguid' => 'e',
'size' => 1,
'postdate' => now(),
'adddate' => now(),
]);
app(ReleaseUpdateService::class)->updateRelease(
$release->fresh(),
$newName,
'Raw file: Flat scene release',
true,
'Filenames, ',
true,
false,
);
$release->refresh();
$this->assertSame($newName, $release->searchname);
$this->assertSame(Category::TV_WEBDL, $release->categories_id);
$this->assertNotSame(Category::MOVIE_WEBDL, $release->categories_id);
$this->assertSame(1, (int) $release->iscategorized);
$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();
+99
View File
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Models\Category;
use App\Services\Categorization\Categorizers\MovieCategorizer;
use App\Services\Categorization\Categorizers\TvCategorizer;
use App\Services\Categorization\ReleaseContext;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class CategorizeFullSeasonTest extends TestCase
{
/**
* @return array<string, array{0: string, 1: int}>
*/
public static function fullSeasonProvider(): array
{
return [
'reported dotted release' => [
'Tale.of.the.Nine.Tailed.S02.2023.1080p.AMZN.WEB-DL.x264.DDP2.0-ADWeb',
Category::TV_WEBDL,
],
'season at start' => [
'S1.2023.Show.Name.1080p.AMZN.WEB-DL.x264-GROUP',
Category::TV_WEBDL,
],
'dash separated season' => [
'Show-Name-S2-2023-1080p-WEB-DL-GROUP',
Category::TV_WEBDL,
],
'underscore separated season' => [
'Show_Name_S12_2023_1080p_WEB-DL_GROUP',
Category::TV_WEBDL,
],
'space separated season' => [
'Show Name S999 2023 1080p WEB-DL GROUP',
Category::TV_WEBDL,
],
'season without quality markers' => [
'Show.Name.S03.2023-GROUP',
Category::TV_OTHER,
],
];
}
#[DataProvider('fullSeasonProvider')]
public function test_standalone_season_tokens_are_categorized_as_tv(string $name, int $expectedCategory): void
{
$context = $this->context($name);
$tvResult = (new TvCategorizer)->categorize($context);
$this->assertTrue($context->hasStandaloneSeasonToken());
$this->assertSame($expectedCategory, $tvResult->categoryId);
$this->assertTrue((new MovieCategorizer)->shouldSkip($context));
$this->assertNotSame(Category::MOVIE_WEBDL, $tvResult->categoryId);
}
public function test_embedded_season_like_text_is_not_treated_as_a_full_season(): void
{
$context = $this->context('Movie.Name.AS02.2023.1080p.AMZN.WEB-DL.x264-GROUP');
$movieCategorizer = new MovieCategorizer;
$this->assertFalse($context->hasStandaloneSeasonToken());
$this->assertFalse($movieCategorizer->shouldSkip($context));
$this->assertSame(Category::MOVIE_WEBDL, $movieCategorizer->categorize($context)->categoryId);
}
public function test_normal_year_based_movie_remains_a_movie(): void
{
$context = $this->context('Oppenheimer.2023.1080p.AMZN.WEB-DL.x264-GROUP');
$movieCategorizer = new MovieCategorizer;
$this->assertFalse($context->hasStandaloneSeasonToken());
$this->assertFalse($movieCategorizer->shouldSkip($context));
$this->assertSame(Category::MOVIE_WEBDL, $movieCategorizer->categorize($context)->categoryId);
}
public function test_existing_episode_pattern_remains_tv_and_is_skipped_by_movies(): void
{
$context = $this->context('Show.Name.S02E10.2023.1080p.AMZN.WEB-DL.x264-GROUP');
$this->assertFalse($context->hasStandaloneSeasonToken());
$this->assertSame(Category::TV_WEBDL, (new TvCategorizer)->categorize($context)->categoryId);
$this->assertTrue((new MovieCategorizer)->shouldSkip($context));
}
private function context(string $name): ReleaseContext
{
return new ReleaseContext(
releaseName: $name,
groupId: 0,
catWebDL: true,
);
}
}