|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace App\Services\Categorization\Categorizers; |
|
4
|
|
|
|
|
5
|
|
|
use App\Models\Category; |
|
6
|
|
|
use App\Services\Categorization\CategorizationResult; |
|
7
|
|
|
use App\Services\Categorization\ReleaseContext; |
|
8
|
|
|
|
|
9
|
|
|
/** |
|
10
|
|
|
* Categorizer for TV content including HD, SD, UHD, Anime, Sports, Documentaries, etc. |
|
11
|
|
|
*/ |
|
12
|
|
|
class TvCategorizer extends AbstractCategorizer |
|
13
|
|
|
{ |
|
14
|
|
|
protected int $priority = 20; |
|
15
|
|
|
|
|
16
|
|
|
public function getName(): string |
|
17
|
|
|
{ |
|
18
|
|
|
return 'TV'; |
|
19
|
|
|
} |
|
20
|
|
|
|
|
21
|
|
|
public function shouldSkip(ReleaseContext $context): bool |
|
22
|
|
|
{ |
|
23
|
|
|
return $context->hasAdultMarkers(); |
|
24
|
|
|
} |
|
25
|
|
|
|
|
26
|
|
|
public function categorize(ReleaseContext $context): CategorizationResult |
|
27
|
|
|
{ |
|
28
|
|
|
$name = $context->releaseName; |
|
29
|
|
|
|
|
30
|
|
|
if (!$this->looksLikeTV($name)) { |
|
31
|
|
|
return $this->noMatch(); |
|
32
|
|
|
} |
|
33
|
|
|
|
|
34
|
|
|
if ($result = $this->checkAnime($name)) { |
|
35
|
|
|
return $result; |
|
36
|
|
|
} |
|
37
|
|
|
if ($result = $this->checkSport($name)) { |
|
38
|
|
|
return $result; |
|
39
|
|
|
} |
|
40
|
|
|
if ($result = $this->checkDocumentary($name)) { |
|
41
|
|
|
return $result; |
|
42
|
|
|
} |
|
43
|
|
|
if ($result = $this->checkForeign($context)) { |
|
44
|
|
|
return $result; |
|
45
|
|
|
} |
|
46
|
|
|
if ($result = $this->checkX265($name)) { |
|
47
|
|
|
return $result; |
|
48
|
|
|
} |
|
49
|
|
|
if ($context->catWebDL && ($result = $this->checkWebDL($name))) { |
|
50
|
|
|
return $result; |
|
51
|
|
|
} |
|
52
|
|
|
if ($result = $this->checkUHD($name)) { |
|
53
|
|
|
return $result; |
|
54
|
|
|
} |
|
55
|
|
|
if ($result = $this->checkHD($name, $context->catWebDL)) { |
|
56
|
|
|
return $result; |
|
57
|
|
|
} |
|
58
|
|
|
if ($result = $this->checkSD($name)) { |
|
59
|
|
|
return $result; |
|
60
|
|
|
} |
|
61
|
|
|
if ($result = $this->checkOther($name)) { |
|
62
|
|
|
return $result; |
|
63
|
|
|
} |
|
64
|
|
|
|
|
65
|
|
|
return $this->noMatch(); |
|
66
|
|
|
} |
|
67
|
|
|
|
|
68
|
|
|
protected function looksLikeTV(string $name): bool |
|
69
|
|
|
{ |
|
70
|
|
|
// Season + Episode pattern: S01E01, S01.E01, S1D1, etc. |
|
71
|
|
|
if (preg_match('/[._ -]s\d{1,3}[._ -]?(e|d(isc)?)\d{1,3}([._ -]|$)/i', $name)) { |
|
72
|
|
|
return true; |
|
73
|
|
|
} |
|
74
|
|
|
// Episode-only pattern: .E01., .E02., E01.1080p (common in anime) |
|
75
|
|
|
if (preg_match('/[._ -]E\d{1,4}[._ -]/i', $name)) { |
|
76
|
|
|
return true; |
|
77
|
|
|
} |
|
78
|
|
|
// Season pack with Complete/Full: S01.Complete, S01.Full |
|
79
|
|
|
if (preg_match('/[._ -]S\d{1,3}[._ -]?(Complete|COMPLETE|Full|FULL)/i', $name)) { |
|
80
|
|
|
return true; |
|
81
|
|
|
} |
|
82
|
|
|
// Season pack with resolution/quality: S01.1080p, S01.720p, S01.2160p, S02.WEB-DL |
|
83
|
|
|
if (preg_match('/[._ -]S\d{1,3}[._ -](480p|720p|1080[pi]|2160p|4K|UHD|WEB|HDTV|BluRay|NF|AMZN|DSNP|ATVP|HMAX)/i', $name)) { |
|
84
|
|
|
return true; |
|
85
|
|
|
} |
|
86
|
|
|
// Episode pattern: Episode 01, Ep.01, Ep 1 |
|
87
|
|
|
if (preg_match('/\b(Episode|Ep)[._ -]?\d{1,4}\b/i', $name)) { |
|
88
|
|
|
return true; |
|
89
|
|
|
} |
|
90
|
|
|
// TV source markers |
|
91
|
|
|
if (preg_match('/\b(HDTV|PDTV|DSR|TVRip|SATRip|DTHRip)\b/i', $name)) { |
|
92
|
|
|
return true; |
|
93
|
|
|
} |
|
94
|
|
|
// Daily show pattern: Show.Name.2024.01.15 or Show.Name.2024-01-15 |
|
95
|
|
|
if (preg_match('/[._ -](19|20)\d{2}[._ -]\d{2}[._ -]\d{2}[._ -]/i', $name)) { |
|
96
|
|
|
return true; |
|
97
|
|
|
} |
|
98
|
|
|
// Known anime release groups (should be treated as TV) |
|
99
|
|
|
if (preg_match('/[.\-_ ](URANiME|ANiHLS|HaiKU|ANiURL|SkyAnime|Erai-raws|LostYears|Vodes|SubsPlease|Judas|Ember|EMBER|YuiSubs|ASW|Tsundere-Raws|Anime-Raws)[.\-_ ]?/i', $name)) { |
|
100
|
|
|
return true; |
|
101
|
|
|
} |
|
102
|
|
|
return false; |
|
103
|
|
|
} |
|
104
|
|
|
|
|
105
|
|
|
protected function checkAnime(string $name): ?CategorizationResult |
|
106
|
|
|
{ |
|
107
|
|
|
if (preg_match('/[._ -]Anime[._ -]/i', $name)) { |
|
108
|
|
|
return $this->matched(Category::TV_ANIME, 0.95, 'anime_pattern'); |
|
109
|
|
|
} |
|
110
|
|
|
// Known anime release groups - now matches anywhere in the name, not just at the end |
|
111
|
|
|
if (preg_match('/[.\-_ ](URANiME|ANiHLS|HaiKU|ANiURL|SkyAnime|Erai-raws|LostYears|Vodes|SubsPlease|Judas|Ember|EMBER|YuiSubs|ASW|Tsundere-Raws|Anime-Raws)[.\-_ ]?/i', $name)) { |
|
112
|
|
|
return $this->matched(Category::TV_ANIME, 0.95, 'anime_group'); |
|
113
|
|
|
} |
|
114
|
|
|
// Anime hash pattern: [GroupName] Title - 01 [ABCD1234] |
|
115
|
|
|
if (preg_match('/^\[.+\].*\d{2,3}.*\[[a-fA-F0-9]{8}\]/i', $name)) { |
|
116
|
|
|
return $this->matched(Category::TV_ANIME, 0.9, 'anime_hash'); |
|
117
|
|
|
} |
|
118
|
|
|
// Japanese title pattern with "no" particle (e.g., Shuumatsu.no.Valkyrie, Shingeki.no.Kyojin) |
|
119
|
|
|
// Combined with episode-only pattern (E05 without season prefix) or roman numeral season |
|
120
|
|
|
if (preg_match('/[._ ]no[._ ]/i', $name) && |
|
121
|
|
|
(preg_match('/[._ ](I{1,3}|IV|V|VI{0,3}|IX|X)[._ ]?E\d{1,4}[._ ]/i', $name) || |
|
122
|
|
|
(preg_match('/[._ ]E\d{1,4}[._ ]/i', $name) && !preg_match('/[._ ]S\d{1,3}[._ ]?E\d/i', $name)))) { |
|
123
|
|
|
return $this->matched(Category::TV_ANIME, 0.9, 'anime_japanese_title'); |
|
124
|
|
|
} |
|
125
|
|
|
// Episode pattern with known anime indicators |
|
126
|
|
|
if (preg_match('/[._ -]E\d{1,4}[._ -]/i', $name) && |
|
127
|
|
|
preg_match('/\b(BluRay|BD|BDRip)\b/i', $name) && |
|
128
|
|
|
!preg_match('/\bS\d{1,3}\b/i', $name)) { |
|
129
|
|
|
// Episode-only pattern with BluRay but no season - likely anime |
|
130
|
|
|
return $this->matched(Category::TV_ANIME, 0.8, 'anime_episode_bluray'); |
|
131
|
|
|
} |
|
132
|
|
|
// Roman numeral season with episode-only pattern (common in anime) |
|
133
|
|
|
// e.g., Title.III.E05, Title.II.E12 - typically anime naming convention |
|
134
|
|
|
if (preg_match('/[._ ](I{1,3}|IV|V|VI{0,3}|IX|X)[._ ]E\d{1,4}[._ ]/i', $name) && |
|
135
|
|
|
!preg_match('/[._ ]S\d{1,3}[._ ]?E\d/i', $name)) { |
|
136
|
|
|
return $this->matched(Category::TV_ANIME, 0.85, 'anime_roman_numeral_season'); |
|
137
|
|
|
} |
|
138
|
|
|
return null; |
|
139
|
|
|
} |
|
140
|
|
|
|
|
141
|
|
|
protected function checkSport(string $name): ?CategorizationResult |
|
142
|
|
|
{ |
|
143
|
|
|
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) && |
|
144
|
|
|
preg_match('/\d{4}|\b(Season|Week|Round|Match|Game)\b/i', $name)) { |
|
145
|
|
|
return $this->matched(Category::TV_SPORT, 0.85, 'sport'); |
|
146
|
|
|
} |
|
147
|
|
|
return null; |
|
148
|
|
|
} |
|
149
|
|
|
|
|
150
|
|
|
protected function checkDocumentary(string $name): ?CategorizationResult |
|
151
|
|
|
{ |
|
152
|
|
|
if (preg_match('/\b(Documentary|Docu[._ -]?Series|DOCU)\b/i', $name)) { |
|
153
|
|
|
return $this->matched(Category::TV_DOCU, 0.85, 'documentary'); |
|
154
|
|
|
} |
|
155
|
|
|
return null; |
|
156
|
|
|
} |
|
157
|
|
|
|
|
158
|
|
|
protected function checkForeign(ReleaseContext $context): ?CategorizationResult |
|
159
|
|
|
{ |
|
160
|
|
|
if (!$context->categorizeForeign) { |
|
161
|
|
|
return null; |
|
162
|
|
|
} |
|
163
|
|
|
if (preg_match('/(danish|flemish|Deutsch|dutch|french|german|hebrew|nl[._ -]?sub|dub(bed|s)?|\.NL|norwegian|swedish|swesub|spanish|Staffel)[._ -]|\(german\)|Multisub/i', $context->releaseName)) { |
|
164
|
|
|
return $this->matched(Category::TV_FOREIGN, 0.8, 'foreign_language'); |
|
165
|
|
|
} |
|
166
|
|
|
return null; |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
protected function checkX265(string $name): ?CategorizationResult |
|
170
|
|
|
{ |
|
171
|
|
|
if (preg_match('/(S\d+).*(x265).*(rmteam|MeGusta|HETeam|PSA|ONLY|H4S5S|TrollHD|ImE)/i', $name)) { |
|
172
|
|
|
return $this->matched(Category::TV_X265, 0.9, 'x265_group'); |
|
173
|
|
|
} |
|
174
|
|
|
return null; |
|
175
|
|
|
} |
|
176
|
|
|
|
|
177
|
|
|
protected function checkWebDL(string $name): ?CategorizationResult |
|
178
|
|
|
{ |
|
179
|
|
|
if (preg_match('/web[._ -]dl|web-?rip/i', $name)) { |
|
180
|
|
|
return $this->matched(Category::TV_WEBDL, 0.85, 'webdl'); |
|
181
|
|
|
} |
|
182
|
|
|
return null; |
|
183
|
|
|
} |
|
184
|
|
|
|
|
185
|
|
|
protected function checkUHD(string $name): ?CategorizationResult |
|
186
|
|
|
{ |
|
187
|
|
|
// Single episode UHD |
|
188
|
|
|
if (preg_match('/S\d+[._ -]?E\d+/i', $name) && preg_match('/2160p/i', $name)) { |
|
189
|
|
|
return $this->matched(Category::TV_UHD, 0.9, 'uhd_episode'); |
|
190
|
|
|
} |
|
191
|
|
|
// Season pack UHD |
|
192
|
|
|
if (preg_match('/[._ -]S\d+[._ -].*2160p/i', $name)) { |
|
193
|
|
|
return $this->matched(Category::TV_UHD, 0.9, 'uhd_season'); |
|
194
|
|
|
} |
|
195
|
|
|
// UHD with streaming service markers |
|
196
|
|
|
if (preg_match('/(S\d+).*(2160p).*(Netflix|Amazon|NF|AMZN).*(TrollUHD|NTb|VLAD|DEFLATE|POFUDUK|CMRG)/i', $name)) { |
|
197
|
|
|
return $this->matched(Category::TV_UHD, 0.9, 'uhd_streaming'); |
|
198
|
|
|
} |
|
199
|
|
|
return null; |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
protected function checkHD(string $name, bool $catWebDL): ?CategorizationResult |
|
203
|
|
|
{ |
|
204
|
|
|
if (preg_match('/1080([ip])|720p|bluray/i', $name)) { |
|
205
|
|
|
return $this->matched(Category::TV_HD, 0.85, 'hd_resolution'); |
|
206
|
|
|
} |
|
207
|
|
|
if (!$catWebDL && preg_match('/web[._ -]dl|web-?rip/i', $name)) { |
|
208
|
|
|
return $this->matched(Category::TV_HD, 0.8, 'hd_webdl_fallback'); |
|
209
|
|
|
} |
|
210
|
|
|
return null; |
|
211
|
|
|
} |
|
212
|
|
|
|
|
213
|
|
|
protected function checkSD(string $name): ?CategorizationResult |
|
214
|
|
|
{ |
|
215
|
|
|
if (preg_match('/(360|480|576)p|Complete[._ -]Season|dvdr(ip)?|dvd5|dvd9|\.pdtv|SD[._ -]TV|TVRip|NTSC|BDRip|hdtv|xvid/i', $name)) { |
|
216
|
|
|
return $this->matched(Category::TV_SD, 0.8, 'sd_format'); |
|
217
|
|
|
} |
|
218
|
|
|
if (preg_match('/(([HP])D[._ -]?TV|DSR|WebRip)[._ -]x264/i', $name)) { |
|
219
|
|
|
return $this->matched(Category::TV_SD, 0.8, 'sd_codec'); |
|
220
|
|
|
} |
|
221
|
|
|
return null; |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
protected function checkOther(string $name): ?CategorizationResult |
|
225
|
|
|
{ |
|
226
|
|
|
// Season + episode pattern |
|
227
|
|
|
if (preg_match('/[._ -]s\d{1,3}[._ -]?(e|d(isc)?)\d{1,3}([._ -]|$)/i', $name)) { |
|
228
|
|
|
return $this->matched(Category::TV_OTHER, 0.6, 'tv_other'); |
|
229
|
|
|
} |
|
230
|
|
|
// Season pack pattern (S01, S02, etc.) with any quality marker |
|
231
|
|
|
if (preg_match('/[._ -]S\d{1,3}[._ -]/i', $name)) { |
|
232
|
|
|
return $this->matched(Category::TV_OTHER, 0.6, 'tv_season_pack'); |
|
233
|
|
|
} |
|
234
|
|
|
return null; |
|
235
|
|
|
} |
|
236
|
|
|
} |
|
237
|
|
|
|