From 4d91912342138e45497585ebc06dcfcd851af22d Mon Sep 17 00:00:00 2001 From: DariusIII Date: Tue, 12 May 2026 15:41:02 +0200 Subject: [PATCH] Update anime releases categorization --- .../Categorizers/GroupNameCategorizer.php | 3 + .../Categorizers/TvCategorizer.php | 46 +++++++- tests/Unit/CategorizeAnimeTest.php | 101 ++++++++++++++++++ 3 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/CategorizeAnimeTest.php diff --git a/app/Services/Categorization/Categorizers/GroupNameCategorizer.php b/app/Services/Categorization/Categorizers/GroupNameCategorizer.php index c3d041941..0734f5390 100644 --- a/app/Services/Categorization/Categorizers/GroupNameCategorizer.php +++ b/app/Services/Categorization/Categorizers/GroupNameCategorizer.php @@ -23,6 +23,9 @@ class GroupNameCategorizer extends AbstractCategorizer if (empty($groupName)) { return $this->noMatch(); } + if (preg_match('/alt\.binaries\..*anime/i', $groupName)) { + return $this->matched(Category::TV_ANIME, 0.8, 'group_name_anime'); + } if (preg_match('/alt\.binaries\..*?(tv|hdtv|tvseries)/i', $groupName)) { return $this->matched(Category::TV_OTHER, 0.6, 'group_name_tv'); } diff --git a/app/Services/Categorization/Categorizers/TvCategorizer.php b/app/Services/Categorization/Categorizers/TvCategorizer.php index d7e87d7e4..993bf4d7b 100644 --- a/app/Services/Categorization/Categorizers/TvCategorizer.php +++ b/app/Services/Categorization/Categorizers/TvCategorizer.php @@ -17,6 +17,12 @@ class TvCategorizer extends AbstractCategorizer 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'; + /** + * Known anime release/fansub group tags. Matches when the tag appears + * as a standalone token in the release name (bracketed, dotted, etc.). + */ + private const string ANIME_GROUPS_REGEX = '/(?:^|[.\-_ \[])(URANiME|ANiHLS|HaiKU|ANiURL|SkyAnime|Erai-raws|LostYears|Vodes|SubsPlease|Judas|Ember|EMBER|YuiSubs|ASW|Tsundere-Raws|Anime-Raws|Kekkan|SSA|MoyaiSubs|QCE|Nyanpasu|UCCUSS|NS|DKB|MTBB|NanDesuKa|smol|Cleo|ToshY|Kawaiika-Raws|Beatrice-Raws|Reaktor|FLE|Commie|HorribleSubs|Doki|FFF|UTW|Coalgirls|CoalGuys|Mezashite|Vivid|Kaylith|Underwater|gg|Chihiro|Hatsuyuki|Mazui|WhyNot|Migoto|Doutei|GJM|Asenshi|Inka-Subs|Akatsuki)(?:[.\-_ \]]|$)/i'; + protected int $priority = 20; public function getName(): string @@ -33,6 +39,13 @@ class TvCategorizer extends AbstractCategorizer { $name = $context->releaseName; + // High-signal anime checks first. These can match release names that + // don't follow standard TV S01E01 naming conventions (e.g. anime + // fansub releases like "[Group] Show - 08 [1080p]"). + if ($result = $this->checkAnimeStrongSignals($name, $context)) { + return $result; + } + if (! $this->looksLikeTV($name)) { return $this->noMatch(); } @@ -106,24 +119,51 @@ class TvCategorizer extends AbstractCategorizer 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)) { + if (preg_match(self::ANIME_GROUPS_REGEX, $name)) { return true; } return false; } + /** + * Strong, gate-bypassing anime signals: Anime Tosho poster, anime usenet + * groups, and anime hash patterns. These can match even when the release + * name doesn't look like a typical TV release. + */ + protected function checkAnimeStrongSignals(string $name, ReleaseContext $context): ?CategorizationResult + { + // Anime Tosho poster (very strong signal) + if ($context->poster !== '' && preg_match('/animetosho\.org/i', $context->poster)) { + return $this->matched(Category::TV_ANIME, 0.98, 'anime_tosho_poster'); + } + // Usenet group dedicated to anime (e.g. alt.binaries.*anime*) + if ($context->groupName !== '' && preg_match('/alt\.binaries\..*anime/i', $context->groupName)) { + return $this->matched(Category::TV_ANIME, 0.9, 'anime_group_name'); + } + // Known anime fansub/release group tag + if (preg_match(self::ANIME_GROUPS_REGEX, $name)) { + return $this->matched(Category::TV_ANIME, 0.92, 'anime_group'); + } + // Anime hash pattern: [GroupName] Title - 01 [ABCD1234] + if (preg_match('/^\[.+\].*\d{2,3}.*\[[a-fA-F0-9]{8}\]/i', $name)) { + return $this->matched(Category::TV_ANIME, 0.9, 'anime_hash'); + } + + return null; + } + protected function checkAnime(string $name, ReleaseContext $context): ?CategorizationResult { // Check for Anime Tosho poster - if (preg_match('/animetosho\.org|usenet\.bot@animetosho\.org/i', $context->poster)) { + if ($context->poster !== '' && preg_match('/animetosho\.org/i', $context->poster)) { return $this->matched(Category::TV_ANIME, 0.98, 'anime_tosho_poster'); } if (preg_match('/[._ -]Anime[._ -]/i', $name)) { return $this->matched(Category::TV_ANIME, 0.95, 'anime_pattern'); } // Known anime release groups - matches with brackets, dots, dashes, underscores, spaces, or at the start - 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)) { + if (preg_match(self::ANIME_GROUPS_REGEX, $name)) { return $this->matched(Category::TV_ANIME, 0.95, 'anime_group'); } // Anime hash pattern: [GroupName] Title - 01 [ABCD1234] diff --git a/tests/Unit/CategorizeAnimeTest.php b/tests/Unit/CategorizeAnimeTest.php new file mode 100644 index 000000000..8a16f3034 --- /dev/null +++ b/tests/Unit/CategorizeAnimeTest.php @@ -0,0 +1,101 @@ + + */ + public static function animeReleaseProvider(): array + { + return [ + 'animetosho poster catches non-TV-looking release' => [ + '[SSA] Cestvs - The Roman Fighter - 08 [480p]', + 'alt.binaries.multimedia.anime.highspeed', + 'Anime Tosho ', + ], + 'animetosho poster catches subsplease release with hash' => [ + '[SubsPlease] Cestvs - The Roman Fighter - 08 (720p) [26BDA3C9]', + 'alt.binaries.multimedia.anime.highspeed', + 'Anime Tosho ', + ], + 'animetosho poster catches yuisubs nvenc release' => [ + '[YuiSubs] Cestvs - The Roman Fighter - 08 (NVENC H.265 1080p)', + 'alt.binaries.multimedia.anime.highspeed', + 'Anime Tosho ', + ], + 'animetosho poster catches nyanpasu hevc release' => [ + '[Nyanpasu] Super Cub - 09 [1080p][HEVC][19475939]', + 'alt.binaries.multimedia.anime.highspeed', + 'Anime Tosho ', + ], + 'anime group name alone is enough' => [ + '[QCE] Kami-tachi ni Hirowareta Otoko - 07', + 'alt.binaries.multimedia.anime.highspeed', + '', + ], + 'known anime release group tag (SSA) without S/E' => [ + '[SSA] Hetalia World Stars - 10 [480p]', + '', + '', + ], + 'known anime release group tag (DKB) with media wrapper' => [ + '[DKB] Kami-tachi ni Hirowareta Otoko - S01E07 [1080p][H.265 10bit]', + '', + '', + ], + 'anime hash pattern alone' => [ + '[NS] Nanatsu No Taizai - Fundo No Shinpan - 21 - (1080p 10bit) [2C014CB1]', + '', + '', + ], + ]; + } + + #[DataProvider('animeReleaseProvider')] + public function test_anime_releases_are_categorized_as_tv_anime(string $name, string $groupName, string $poster): void + { + $categorizer = new TvCategorizer; + + $context = new ReleaseContext( + releaseName: $name, + groupId: 0, + groupName: $groupName, + poster: $poster, + ); + + $result = $categorizer->categorize($context); + + $this->assertTrue($result->isSuccessful(), "Expected anime match for: {$name}"); + $this->assertSame(Category::TV_ANIME, $result->categoryId, "Expected TV_ANIME for: {$name} (matched_by={$result->matchedBy})"); + } + + public function test_group_name_categorizer_classifies_anime_usenet_group(): void + { + $categorizer = new GroupNameCategorizer; + + $context = new ReleaseContext( + releaseName: 'some.random.release.name', + groupId: 0, + groupName: 'alt.binaries.multimedia.anime.highspeed', + poster: '', + ); + + $result = $categorizer->categorize($context); + + $this->assertTrue($result->isSuccessful()); + $this->assertSame(Category::TV_ANIME, $result->categoryId); + $this->assertSame('group_name_anime', $result->matchedBy); + } +} +