|
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 PC content (Games, Software, ISO, 0day, Mac, Phone apps). |
|
11
|
|
|
*/ |
|
12
|
|
|
class PcCategorizer extends AbstractCategorizer |
|
13
|
|
|
{ |
|
14
|
|
|
protected int $priority = 30; |
|
15
|
|
|
|
|
16
|
|
|
// Common PC game release groups (matched at end of release name only) |
|
17
|
|
|
protected const PC_GROUPS = '0x0007|ALiAS|ANOMALY|BACKLASH|BAT|BLASTCiTY|CHRONOS|CODEX|CONSPIRACY|CPY|CROSSFiRE|DARKS(?:iDERS|IDERS)|DELiGHT|DEViANCE|DINOByTES|DOGE|DODI|DVN|ELAMIGOS|EMPRESS|ENiGMA|FANiSO|FAS(?:DOX|iSO)|FCKDRM|FITGIRL|FLT|FLTDOX|GGS|GOG(?:-?GAMES)?|GOLDBERG|HATRED|HI2U|HOODLUM|I_KnoW|iNLAWS|iNTERNAL|JAGUAR|KACZKRULL|KAOS|KaOsKrew|LAZARUS|LiGHTFORCE|MEPHISTO|OUTLAWS|PARADOX|PHOENIX|PLAZA|POSTMORTEM|PROPHET|PROVOKED|RADIUM|RAZOR(?:1911|DOX)|RELOADED|REVOLUTiON|RUNE|SAFARi|SCRUBS|SiLENTGATE|SiMPLEX|SKIDROW|SPLiNTERCELL|STEAMPUNKS|SUXXORS|TENOKE|TiNYiSO|UBERMENCH|UNLEASHED|VENGEANCE|ViTALiTY|VENOM|VACE|ZEKE|P2P'; |
|
18
|
|
|
|
|
19
|
|
|
// PC-only keywords (can appear anywhere) |
|
20
|
|
|
protected const PC_KEYWORDS = 'PC[ _.-]?GAMES?|\[(?:PC)\]|\(PC\)|Steam(?:[ ._-]?Rip|\b)|Retail\s*PC|DRM-?Free|Win(All|32|64)\b|Repack'; |
|
21
|
|
|
|
|
22
|
|
|
public function getName(): string |
|
23
|
|
|
{ |
|
24
|
|
|
return 'PC'; |
|
25
|
|
|
} |
|
26
|
|
|
|
|
27
|
|
|
public function shouldSkip(ReleaseContext $context): bool |
|
28
|
|
|
{ |
|
29
|
|
|
if ($context->hasAdultMarkers()) return true; |
|
30
|
|
|
// Skip TV shows (season patterns) |
|
31
|
|
|
if (preg_match('/[._ -]S\d{1,3}[._ -]?(E\d|Complete|Full|1080|720|480|2160|WEB|HDTV|BluRay)/i', $context->releaseName)) return true; |
|
32
|
|
|
return false; |
|
33
|
|
|
} |
|
34
|
|
|
|
|
35
|
|
|
public function categorize(ReleaseContext $context): CategorizationResult |
|
36
|
|
|
{ |
|
37
|
|
|
$name = $context->releaseName; |
|
38
|
|
|
|
|
39
|
|
|
// Try each PC category |
|
40
|
|
|
if ($result = $this->checkPhone($name)) { |
|
41
|
|
|
return $result; |
|
42
|
|
|
} |
|
43
|
|
|
|
|
44
|
|
|
if ($result = $this->checkMac($name)) { |
|
45
|
|
|
return $result; |
|
46
|
|
|
} |
|
47
|
|
|
|
|
48
|
|
|
if ($result = $this->checkPCGame($name, $context->poster)) { |
|
49
|
|
|
return $result; |
|
50
|
|
|
} |
|
51
|
|
|
|
|
52
|
|
|
if ($result = $this->checkISO($name)) { |
|
53
|
|
|
return $result; |
|
54
|
|
|
} |
|
55
|
|
|
|
|
56
|
|
|
if ($result = $this->check0day($name)) { |
|
57
|
|
|
return $result; |
|
58
|
|
|
} |
|
59
|
|
|
|
|
60
|
|
|
return $this->noMatch(); |
|
61
|
|
|
} |
|
62
|
|
|
|
|
63
|
|
|
protected function checkPhone(string $name): ?CategorizationResult |
|
64
|
|
|
{ |
|
65
|
|
|
// iOS |
|
66
|
|
|
if (preg_match('/[^a-z0-9](IPHONE|ITOUCH|IPAD)[._ -]/i', $name)) { |
|
67
|
|
|
return $this->matched(Category::PC_PHONE_IOS, 0.9, 'ios'); |
|
68
|
|
|
} |
|
69
|
|
|
|
|
70
|
|
|
// Android |
|
71
|
|
|
if (preg_match('/[._ -]?(ANDROID)[._ -]/i', $name)) { |
|
72
|
|
|
return $this->matched(Category::PC_PHONE_ANDROID, 0.9, 'android'); |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
// Other mobile platforms |
|
76
|
|
|
if (preg_match('/[^a-z0-9](symbian|xscale|wm5|wm6)[._ -]/i', $name)) { |
|
77
|
|
|
return $this->matched(Category::PC_PHONE_OTHER, 0.85, 'phone_other'); |
|
78
|
|
|
} |
|
79
|
|
|
|
|
80
|
|
|
return null; |
|
81
|
|
|
} |
|
82
|
|
|
|
|
83
|
|
|
protected function checkMac(string $name): ?CategorizationResult |
|
84
|
|
|
{ |
|
85
|
|
|
if (preg_match('/(\b|[._ -])mac([\.\s])?osx(\b|[\-_. ])/i', $name)) { |
|
86
|
|
|
return $this->matched(Category::PC_MAC, 0.9, 'mac'); |
|
87
|
|
|
} |
|
88
|
|
|
|
|
89
|
|
|
if (preg_match('/\b(Mac\s?OS\s?X|macOS)\b/i', $name)) { |
|
90
|
|
|
return $this->matched(Category::PC_MAC, 0.9, 'macos'); |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
return null; |
|
94
|
|
|
} |
|
95
|
|
|
|
|
96
|
|
|
protected function checkPCGame(string $name, string $poster): ?CategorizationResult |
|
97
|
|
|
{ |
|
98
|
|
|
// Exclude console releases |
|
99
|
|
|
$consoleOrMac = '/\b(PS5|PS4|PS3|PlayStation|PS(Vita|V)\b|Xbox\s?(Series|One|360)|XBOX(ONE|360|SERIES|SX|SS)?|XSX|XSS|XBSX|NSW|Switch|WiiU|Wii|3DS|NDS|PSP|PSV(ita)?|GameCube|NGC|CUSA\d{5}|XCI|NSP|PKG)\b/i'; |
|
100
|
|
|
if (preg_match($consoleOrMac, $name) || preg_match('/\b(Mac\s?OS\s?X|macOS)\b/i', $name)) { |
|
101
|
|
|
return null; |
|
102
|
|
|
} |
|
103
|
|
|
|
|
104
|
|
|
// Exclude TV shows |
|
105
|
|
|
$tvPatterns = '/\b(S\d{1,4}[._ -]?E\d{1,4}|S\d{1,4}[._ -]?D\d{1,4}|\d{1,2}x\d{2,3}|Season[._ -]?\d{1,3}|Episode[._ -]?\d{1,4}|HDTV|PDTV|DSR|WEB[._ -]?DL|WEB[._ -]?RIP|TVRip)\b/i'; |
|
106
|
|
|
if (preg_match($tvPatterns, $name)) { |
|
107
|
|
|
return null; |
|
108
|
|
|
} |
|
109
|
|
|
|
|
110
|
|
|
// Check for PC game patterns |
|
111
|
|
|
// PC_GROUPS must be at the END of the release name (scene naming: Game.Name-GROUP) |
|
112
|
|
|
// PC_KEYWORDS can appear anywhere |
|
113
|
|
|
$groupPattern = '/[-.](' . self::PC_GROUPS . ')$/i'; |
|
114
|
|
|
$keywordPattern = '/' . self::PC_KEYWORDS . '/i'; |
|
115
|
|
|
|
|
116
|
|
|
if (preg_match($groupPattern, $name) || preg_match($keywordPattern, $name)) { |
|
117
|
|
|
return $this->matched(Category::PC_GAMES, 0.9, 'pc_game'); |
|
118
|
|
|
} |
|
119
|
|
|
|
|
120
|
|
|
// Check poster |
|
121
|
|
|
if (preg_match('/<PC@MASTER\.RACE>/i', $poster)) { |
|
122
|
|
|
return $this->matched(Category::PC_GAMES, 0.85, 'pc_game_poster'); |
|
123
|
|
|
} |
|
124
|
|
|
|
|
125
|
|
|
return null; |
|
126
|
|
|
} |
|
127
|
|
|
|
|
128
|
|
|
protected function checkISO(string $name): ?CategorizationResult |
|
129
|
|
|
{ |
|
130
|
|
|
if (preg_match('/[._ -]([a-zA-Z]{2,10})?iso[ _.-]|[\-. ]([a-z]{2,10})?iso$/i', $name)) { |
|
131
|
|
|
return $this->matched(Category::PC_ISO, 0.85, 'iso'); |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
// Training/Tutorial ISOs |
|
135
|
|
|
if (preg_match('/[._ -](DYNAMiCS|INFINITESKILLS|UDEMY|kEISO|PLURALSIGHT|DIGITALTUTORS|TUTSPLUS|OSTraining|PRODEV|CBT\.Nuggets|COMPRISED)/i', $name)) { |
|
136
|
|
|
return $this->matched(Category::PC_ISO, 0.9, 'training_iso'); |
|
137
|
|
|
} |
|
138
|
|
|
|
|
139
|
|
|
return null; |
|
140
|
|
|
} |
|
141
|
|
|
|
|
142
|
|
|
protected function check0day(string $name): ?CategorizationResult |
|
143
|
|
|
{ |
|
144
|
|
|
// Explicit 0day indicators |
|
145
|
|
|
if (preg_match('/[._ -]exe$|[._ -](utorrent|Virtualbox)[._ -]|\b0DAY\b|incl.+crack| DRM$|>DRM</i', $name)) { |
|
146
|
|
|
return $this->matched(Category::PC_0DAY, 0.9, '0day_explicit'); |
|
147
|
|
|
} |
|
148
|
|
|
|
|
149
|
|
|
// System/architecture indicators |
|
150
|
|
|
if (preg_match('/[._ -]((32|64)bit|converter|i\d86|key(gen|maker)|freebsd|GAMEGUiDE|hpux|irix|linux|multilingual|Patch|Pro v\d{1,3}|portable|regged|software|solaris|template|unix|win2kxp2k3|win64|win(2k|32|64|all|dows|nt(2k)?(xp)?|xp)|win9x(me|nt)?|x(32|64|86))[._ -]/i', $name)) { |
|
151
|
|
|
return $this->matched(Category::PC_0DAY, 0.85, '0day_system'); |
|
152
|
|
|
} |
|
153
|
|
|
|
|
154
|
|
|
// Software vendors and patterns |
|
155
|
|
|
if (preg_match('/\b(Adobe|auto(cad|desk)|-BEAN|Cracked|Cucusoft|CYGNUS|Divx[._ -]Plus|\.(deb|exe)|DIGERATI|FOSI|-FONT|Key(filemaker|gen|maker)|Lynda\.com|lz0|MULTiLANGUAGE|Microsoft\s*(Office|Windows|Server)|MultiOS|-(iNViSiBLE|SPYRAL|SUNiSO|UNION|TE)|v\d{1,3}.*?Pro|[._ -]v\d{1,3}[._ -]|\(x(64|86)\)|Xilisoft)\b/i', $name)) { |
|
156
|
|
|
return $this->matched(Category::PC_0DAY, 0.85, '0day_software'); |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
return null; |
|
160
|
|
|
} |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
|