mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 00:01:21 +00:00
Create categorization service
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ use App\Models\Release;
|
||||
use App\Models\Settings;
|
||||
use App\Models\UsenetGroup;
|
||||
use App\Services\BlacklistService;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use Blacklight\utility\Utility;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -27,7 +28,7 @@ class NZBImport
|
||||
*/
|
||||
protected mixed $crossPostt;
|
||||
|
||||
protected Categorize $category;
|
||||
protected CategorizationService $category;
|
||||
|
||||
/**
|
||||
* List of all the group names/ids in the DB.
|
||||
@@ -67,7 +68,7 @@ class NZBImport
|
||||
{
|
||||
$this->echoCLI = config('nntmux.echocli');
|
||||
$this->blacklistService = new BlacklistService;
|
||||
$this->category = new Categorize;
|
||||
$this->category = new CategorizationService();
|
||||
$this->nzb = new NZB;
|
||||
$this->releaseCleaner = new ReleaseCleaning;
|
||||
$this->colorCli = new ColorCLI;
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Category;
|
||||
use App\Models\Predb;
|
||||
use App\Models\Release;
|
||||
use App\Models\UsenetGroup;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use Blacklight\utility\Utility;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -104,7 +105,7 @@ class NameFixer
|
||||
public ColorCLI $colorCLI;
|
||||
|
||||
/**
|
||||
* @var Categorize
|
||||
* @var CategorizationService
|
||||
*/
|
||||
public mixed $category;
|
||||
|
||||
@@ -131,7 +132,7 @@ class NameFixer
|
||||
$this->_fileName = '';
|
||||
$this->done = $this->matched = false;
|
||||
$this->colorCLI = new ColorCLI;
|
||||
$this->category = new Categorize;
|
||||
$this->category = new CategorizationService();
|
||||
$this->manticore = new ManticoreSearch;
|
||||
$this->elasticsearch = new ElasticSearchSiteSearch;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ use App\Models\MusicInfo;
|
||||
use App\Models\Release;
|
||||
use App\Models\Settings;
|
||||
use App\Models\UsenetGroup;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use App\Services\CollectionCleanupService;
|
||||
use App\Services\ReleaseCreationService;
|
||||
use Blacklight\Categorize;
|
||||
use Blacklight\ColorCLI;
|
||||
use Blacklight\Genres;
|
||||
use Blacklight\NNTP;
|
||||
@@ -190,7 +190,7 @@ class ProcessReleases
|
||||
*/
|
||||
public function categorizeRelease(string $type, $groupId): int
|
||||
{
|
||||
$cat = new Categorize;
|
||||
$cat = new CategorizationService();
|
||||
$categorized = $total = 0;
|
||||
$releasesQuery = Release::query()->where(['categories_id' => Category::OTHER_MISC, 'iscategorized' => 0]);
|
||||
if (! empty($groupId)) {
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\Models\Release;
|
||||
use App\Models\ReleaseFile;
|
||||
use App\Models\Settings;
|
||||
use App\Models\UsenetGroup;
|
||||
use Blacklight\Categorize;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use Blacklight\ColorCLI;
|
||||
use Blacklight\ElasticSearchSiteSearch;
|
||||
use Blacklight\ManticoreSearch;
|
||||
@@ -111,7 +111,7 @@ class ProcessAdditional
|
||||
|
||||
protected NNTP $_nntp;
|
||||
|
||||
protected Categorize $_categorize;
|
||||
protected CategorizationService $_categorize;
|
||||
|
||||
protected NameFixer $_nameFixer;
|
||||
|
||||
@@ -276,7 +276,7 @@ class ProcessAdditional
|
||||
|
||||
$this->_nzb = new NZB;
|
||||
$this->_archiveInfo = new ArchiveInfo;
|
||||
$this->_categorize = new Categorize;
|
||||
$this->_categorize = new CategorizationService();
|
||||
$this->_nameFixer = new NameFixer;
|
||||
$this->_releaseExtra = new ReleaseExtra;
|
||||
$this->_releaseImage = new ReleaseImage;
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use Blacklight\Categorize;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RecategorizeReleases extends Command
|
||||
@@ -66,7 +66,7 @@ class RecategorizeReleases extends Command
|
||||
|
||||
$count = $countQuery->count();
|
||||
|
||||
$cat = new Categorize;
|
||||
$cat = new CategorizationService();
|
||||
$results = $countQuery->select(['id', 'searchname', 'fromname', 'groups_id', 'categories_id'])->get();
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class TestCategorization extends Command
|
||||
{
|
||||
protected $signature = 'nntmux:test-categorization
|
||||
{--release= : Release name to test}
|
||||
{--compare : Compare pipeline vs legacy categorizer}
|
||||
{--list-categorizers : List all registered categorizers}';
|
||||
|
||||
protected $description = 'Test the new pipeline-based categorization service';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$service = new CategorizationService();
|
||||
|
||||
if ($this->option('list-categorizers')) {
|
||||
$this->listCategorizers($service);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$releaseName = $this->option('release');
|
||||
|
||||
if (!$releaseName) {
|
||||
// Test with sample releases
|
||||
$this->testSampleReleases($service);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($this->option('compare')) {
|
||||
$this->compareResult($service, $releaseName);
|
||||
} else {
|
||||
$this->categorizeRelease($service, $releaseName);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function listCategorizers(CategorizationService $service): void
|
||||
{
|
||||
$stats = $service->getCategorizerStats();
|
||||
|
||||
$this->info('Registered Categorizers:');
|
||||
$this->table(
|
||||
['Name', 'Priority', 'Class'],
|
||||
collect($stats)->map(fn ($s) => [$s['name'], $s['priority'], $s['class']])->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
protected function categorizeRelease(CategorizationService $service, string $releaseName): void
|
||||
{
|
||||
$result = $service->determineCategory(0, $releaseName, '', true);
|
||||
|
||||
$this->info("Release: {$releaseName}");
|
||||
$this->info("Category ID: {$result['categories_id']}");
|
||||
|
||||
if (isset($result['debug'])) {
|
||||
$this->info("Matched By: {$result['debug']['matched_by']}");
|
||||
$this->info("Confidence: " . ($result['debug']['final_confidence'] ?? 'N/A'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function compareResult(CategorizationService $service, string $releaseName): void
|
||||
{
|
||||
$comparison = $service->compare(0, $releaseName);
|
||||
|
||||
$this->info("Release: {$releaseName}");
|
||||
$this->newLine();
|
||||
|
||||
$matchStatus = $comparison['match'] ? '<fg=green>MATCH</>' : '<fg=red>MISMATCH</>';
|
||||
$this->line("Result: {$matchStatus}");
|
||||
$this->newLine();
|
||||
|
||||
$this->table(
|
||||
['', 'Pipeline', 'Legacy'],
|
||||
[
|
||||
['Category ID', $comparison['pipeline']['category_id'], $comparison['legacy']['category_id']],
|
||||
['Category Name', $comparison['pipeline']['category_name'], $comparison['legacy']['category_name']],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected function testSampleReleases(CategorizationService $service): void
|
||||
{
|
||||
$samples = [
|
||||
// TV
|
||||
'Game.of.Thrones.S08E06.720p.BluRay.x264-DEMAND',
|
||||
'The.Mandalorian.S03E08.2160p.WEB-DL.DDP5.1.Atmos.H.265-FLUX',
|
||||
'[SubsPlease] Jujutsu Kaisen - 47 (1080p) [ABC12345].mkv',
|
||||
|
||||
// Movies
|
||||
'The.Matrix.Resurrections.2021.2160p.UHD.BluRay.x265-SURCODE',
|
||||
'Inception.2010.1080p.BluRay.x264-SPARKS',
|
||||
'Oppenheimer.2023.WEB-DL.1080p.H.264.AAC-LOL',
|
||||
|
||||
// XXX
|
||||
'Brazzers.23.11.15.Model.Name.XXX.1080p.MP4-KTR',
|
||||
'SexBabesVR.23.10.20.Virtual.Reality.VR180.3D.SBS.2160p-VRSins',
|
||||
|
||||
// Games
|
||||
'Cyberpunk.2077.Ultimate.Edition.v2.12.1-RUNE',
|
||||
'Elden.Ring.Shadow.of.the.Erdtree.PS5-DUPLEX',
|
||||
|
||||
// Music
|
||||
'Taylor.Swift.Midnights.2022.FLAC-dL',
|
||||
'Various.Artists.Now.Thats.What.I.Call.Music.100.2018.MP3.320kbps',
|
||||
|
||||
// Books
|
||||
'Brandon.Sanderson.Mistborn.Trilogy.EPUB',
|
||||
'OReilly.Learning.Python.6th.Edition.PDF',
|
||||
];
|
||||
|
||||
$this->info('Testing sample releases with both pipeline and legacy categorizers:');
|
||||
$this->newLine();
|
||||
|
||||
$results = [];
|
||||
foreach ($samples as $sample) {
|
||||
$comparison = $service->compare(0, $sample);
|
||||
$results[] = [
|
||||
substr($sample, 0, 50) . (strlen($sample) > 50 ? '...' : ''),
|
||||
$comparison['pipeline']['category_name'],
|
||||
$comparison['legacy']['category_name'],
|
||||
$comparison['match'] ? '✓' : '✗',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Release', 'Pipeline', 'Legacy', 'Match'],
|
||||
$results
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use App\Services\Categorization\CategorizationPipeline;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static array categorize(int|string $groupId, string $releaseName, ?string $poster = '', bool $debug = false)
|
||||
* @method static \Illuminate\Support\Collection getCategorizers()
|
||||
* @method static CategorizationPipeline addCategorizer(\App\Services\Categorization\Contracts\CategorizerInterface $categorizer)
|
||||
*
|
||||
* @see \App\Services\Categorization\CategorizationPipeline
|
||||
*/
|
||||
class Categorization extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return CategorizationPipeline::class;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\Categorization\CategorizationPipeline;
|
||||
use App\Services\Categorization\Categorizers;
|
||||
use App\Services\Categorization\Contracts\CategorizerInterface;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class CategorizationServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Register the pipeline as a singleton
|
||||
$this->app->singleton(CategorizationPipeline::class, function ($app) {
|
||||
return CategorizationPipeline::createDefault();
|
||||
});
|
||||
|
||||
// Alias for easier access
|
||||
$this->app->alias(CategorizationPipeline::class, 'categorization');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Settings;
|
||||
use App\Models\UsenetGroup;
|
||||
use App\Services\Categorization\Contracts\CategorizerInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Pipeline-based categorization service.
|
||||
*
|
||||
* This service orchestrates multiple categorizers to determine the best
|
||||
* category for a release. Each categorizer is responsible for a specific
|
||||
* category domain and returns a result with a confidence score.
|
||||
*/
|
||||
class CategorizationPipeline
|
||||
{
|
||||
/**
|
||||
* @var Collection<CategorizerInterface>
|
||||
*/
|
||||
protected Collection $categorizers;
|
||||
|
||||
protected bool $categorizeForeign;
|
||||
protected bool $catWebDL;
|
||||
|
||||
/**
|
||||
* @param iterable<CategorizerInterface> $categorizers
|
||||
*/
|
||||
public function __construct(iterable $categorizers = [])
|
||||
{
|
||||
$this->categorizers = collect($categorizers)
|
||||
->sortBy(fn (CategorizerInterface $c) => $c->getPriority());
|
||||
|
||||
$this->categorizeForeign = (bool) Settings::settingValue('categorizeforeign');
|
||||
$this->catWebDL = (bool) Settings::settingValue('catwebdl');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a categorizer in the pipeline.
|
||||
*/
|
||||
public function addCategorizer(CategorizerInterface $categorizer): self
|
||||
{
|
||||
$this->categorizers->push($categorizer);
|
||||
$this->categorizers = $this->categorizers->sortBy(fn (CategorizerInterface $c) => $c->getPriority());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the category for a release.
|
||||
*
|
||||
* @param int|string $groupId The usenet group ID
|
||||
* @param string $releaseName The name of the release
|
||||
* @param string|null $poster The poster name
|
||||
* @param bool $debug Whether to include debug information
|
||||
* @return array The categorization result
|
||||
*/
|
||||
public function categorize(
|
||||
int|string $groupId,
|
||||
string $releaseName,
|
||||
?string $poster = '',
|
||||
bool $debug = false
|
||||
): array {
|
||||
$groupName = UsenetGroup::whereId($groupId)->value('name') ?? '';
|
||||
|
||||
$context = new ReleaseContext(
|
||||
releaseName: $releaseName,
|
||||
groupId: $groupId,
|
||||
groupName: $groupName,
|
||||
poster: $poster ?? '',
|
||||
categorizeForeign: $this->categorizeForeign,
|
||||
catWebDL: $this->catWebDL,
|
||||
);
|
||||
|
||||
$bestResult = CategorizationResult::noMatch();
|
||||
$allResults = [];
|
||||
|
||||
foreach ($this->categorizers as $categorizer) {
|
||||
// Skip if categorizer determines it shouldn't process this release
|
||||
if ($categorizer->shouldSkip($context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $categorizer->categorize($context);
|
||||
|
||||
if ($debug) {
|
||||
$allResults[$categorizer->getName()] = [
|
||||
'category_id' => $result->categoryId,
|
||||
'confidence' => $result->confidence,
|
||||
'matched_by' => $result->matchedBy,
|
||||
];
|
||||
}
|
||||
|
||||
// If this result is better than our current best, use it
|
||||
if ($result->isSuccessful() && $result->shouldOverride($bestResult)) {
|
||||
$bestResult = $result;
|
||||
|
||||
// If we have a very high confidence match, we can stop early
|
||||
if ($result->confidence >= 0.95) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the return array
|
||||
$returnValue = ['categories_id' => $bestResult->categoryId];
|
||||
|
||||
if ($debug) {
|
||||
$returnValue['debug'] = [
|
||||
'final_category' => $bestResult->categoryId,
|
||||
'final_confidence' => $bestResult->confidence,
|
||||
'matched_by' => $bestResult->matchedBy,
|
||||
'release_name' => $releaseName,
|
||||
'group_name' => $groupName,
|
||||
'all_results' => $allResults,
|
||||
'categorizer_details' => $bestResult->debug,
|
||||
];
|
||||
}
|
||||
|
||||
return $returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered categorizers.
|
||||
*
|
||||
* @return Collection<CategorizerInterface>
|
||||
*/
|
||||
public function getCategorizers(): Collection
|
||||
{
|
||||
return $this->categorizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default pipeline with all standard categorizers.
|
||||
*/
|
||||
public static function createDefault(): self
|
||||
{
|
||||
return new self([
|
||||
new Categorizers\GroupNameCategorizer(),
|
||||
new Categorizers\XxxCategorizer(),
|
||||
new Categorizers\TvCategorizer(),
|
||||
new Categorizers\MovieCategorizer(),
|
||||
new Categorizers\BookCategorizer(),
|
||||
new Categorizers\MusicCategorizer(),
|
||||
new Categorizers\PcCategorizer(),
|
||||
new Categorizers\ConsoleCategorizer(),
|
||||
new Categorizers\MiscCategorizer(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization;
|
||||
|
||||
use App\Models\Category;
|
||||
|
||||
/**
|
||||
* Value object representing the result of a categorization attempt.
|
||||
*/
|
||||
class CategorizationResult
|
||||
{
|
||||
/**
|
||||
* @param int $categoryId The determined category ID
|
||||
* @param float $confidence Confidence level (0.0 to 1.0)
|
||||
* @param string $matchedBy Description of what matched
|
||||
* @param array $debug Additional debug information
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $categoryId = Category::OTHER_MISC,
|
||||
public readonly float $confidence = 0.0,
|
||||
public readonly string $matchedBy = 'none',
|
||||
public readonly array $debug = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Check if this result represents a successful categorization.
|
||||
*/
|
||||
public function isSuccessful(): bool
|
||||
{
|
||||
return $this->categoryId !== Category::OTHER_MISC && $this->confidence > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this result should take precedence over another.
|
||||
*/
|
||||
public function shouldOverride(CategorizationResult $other): bool
|
||||
{
|
||||
// Higher confidence always wins
|
||||
if ($this->confidence > $other->confidence) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If same confidence, prefer non-misc categories
|
||||
if ($this->confidence === $other->confidence) {
|
||||
return $this->categoryId !== Category::OTHER_MISC && $other->categoryId === Category::OTHER_MISC;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a failed/empty result.
|
||||
*/
|
||||
public static function noMatch(): self
|
||||
{
|
||||
return new self(Category::OTHER_MISC, 0.0, 'no_match');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a result with debug info merged.
|
||||
*/
|
||||
public function withDebug(array $additionalDebug): self
|
||||
{
|
||||
return new self(
|
||||
$this->categoryId,
|
||||
$this->confidence,
|
||||
$this->matchedBy,
|
||||
array_merge($this->debug, $additionalDebug)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization;
|
||||
|
||||
use App\Models\Category;
|
||||
|
||||
/**
|
||||
* Categorization service using the new pipeline-based system.
|
||||
*
|
||||
* This class is a drop-in replacement for the legacy Blacklight\Categorize
|
||||
* with additional features like confidence scoring and debug information.
|
||||
*/
|
||||
class CategorizationService
|
||||
{
|
||||
protected CategorizationPipeline $pipeline;
|
||||
|
||||
public function __construct(?CategorizationPipeline $pipeline = null)
|
||||
{
|
||||
$this->pipeline = $pipeline ?? CategorizationPipeline::createDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine category for a release.
|
||||
*
|
||||
* @param int|string $groupId The usenet group ID
|
||||
* @param string $releaseName The name of the release
|
||||
* @param string|null $poster The poster name
|
||||
* @param bool $debug Whether to include debug information
|
||||
* @return array The categorization result with category ID and optional debug info
|
||||
*/
|
||||
public function determineCategory(
|
||||
int|string $groupId,
|
||||
string $releaseName = '',
|
||||
?string $poster = '',
|
||||
bool $debug = false
|
||||
): array {
|
||||
return $this->pipeline->categorize($groupId, $releaseName, $poster, $debug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch categorize multiple releases.
|
||||
*
|
||||
* @param array $releases Array of ['group_id' => x, 'name' => y, 'poster' => z]
|
||||
* @return array Array of categorization results
|
||||
*/
|
||||
public function batchCategorize(array $releases): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($releases as $release) {
|
||||
$groupId = $release['group_id'] ?? $release['groupId'] ?? 0;
|
||||
$name = $release['name'] ?? $release['releaseName'] ?? '';
|
||||
$poster = $release['poster'] ?? '';
|
||||
|
||||
$results[] = [
|
||||
'release' => $release,
|
||||
'result' => $this->determineCategory($groupId, $name, $poster),
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying pipeline.
|
||||
*/
|
||||
public function getPipeline(): CategorizationPipeline
|
||||
{
|
||||
return $this->pipeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom categorizer to the pipeline.
|
||||
*/
|
||||
public function addCategorizer(Contracts\CategorizerInterface $categorizer): self
|
||||
{
|
||||
$this->pipeline->addCategorizer($categorizer);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about categorizer usage.
|
||||
*/
|
||||
public function getCategorizerStats(): array
|
||||
{
|
||||
$categorizers = $this->pipeline->getCategorizers();
|
||||
|
||||
return $categorizers->map(function ($categorizer) {
|
||||
return [
|
||||
'name' => $categorizer->getName(),
|
||||
'priority' => $categorizer->getPriority(),
|
||||
'class' => get_class($categorizer),
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\Contracts\CategorizerInterface;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
abstract class AbstractCategorizer implements CategorizerInterface
|
||||
{
|
||||
protected int $priority = 50;
|
||||
public function getPriority(): int { return $this->priority; }
|
||||
public function shouldSkip(ReleaseContext $context): bool { return false; }
|
||||
protected function matched(int $categoryId, float $confidence, string $matchedBy, array $debug = []): CategorizationResult
|
||||
{
|
||||
return new CategorizationResult($categoryId, $confidence, $matchedBy, $debug);
|
||||
}
|
||||
protected function noMatch(): CategorizationResult { return CategorizationResult::noMatch(); }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
class BookCategorizer extends AbstractCategorizer
|
||||
{
|
||||
protected int $priority = 45;
|
||||
public function getName(): string { return 'Book'; }
|
||||
public function shouldSkip(ReleaseContext $context): bool
|
||||
{
|
||||
if ($context->hasAdultMarkers()) return true;
|
||||
if (preg_match('/\.PS4-[A-Z0-9]+$/i', $context->releaseName)) return true;
|
||||
if (preg_match('/\b(?:PS[1-5]|PlayStation|Xbox|Switch|Nintendo|Wii|3DS|GameCube)\b/i', $context->releaseName)) return true;
|
||||
// Skip TV shows (season patterns)
|
||||
if (preg_match('/[._ -]S\d{1,3}[._ -]?(E\d|Complete|Full|1080|720|480|2160|WEB|HDTV|BluRay)/i', $context->releaseName)) return true;
|
||||
// Skip movies (year + quality patterns)
|
||||
if (preg_match('/\b(19|20)\d{2}\b.*\b(1080p|720p|2160p|BluRay|WEB-DL|BDRip|DVDRip)\b/i', $context->releaseName)) return true;
|
||||
return false;
|
||||
}
|
||||
public function categorize(ReleaseContext $context): CategorizationResult
|
||||
{
|
||||
$name = $context->releaseName;
|
||||
if ($result = $this->checkComic($name)) return $result;
|
||||
if ($result = $this->checkTechnical($name)) return $result;
|
||||
if ($result = $this->checkMagazine($name)) return $result;
|
||||
if ($result = $this->checkEbook($name)) return $result;
|
||||
return $this->noMatch();
|
||||
}
|
||||
protected function checkComic(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\b(?:CBR|CBZ|C2C)\b|\.(?:cbr|cbz)$/i', $name)) return $this->matched(Category::BOOKS_COMICS, 0.9, 'comic_format');
|
||||
if (preg_match('/\b(?:Marvel|DC[._ -]Comics|Image[._ -]Comics|Dark[._ -]Horse|IDW)\b/i', $name) &&
|
||||
preg_match('/\b(?:Comics?|Annual|Issue|Vol|TPB)\b/i', $name)) return $this->matched(Category::BOOKS_COMICS, 0.85, 'comic_publisher');
|
||||
if (preg_match('/\b(?:Manga|Manhwa|Manhua|Webtoon)\b/i', $name)) return $this->matched(Category::BOOKS_COMICS, 0.85, 'manga');
|
||||
return null;
|
||||
}
|
||||
protected function checkTechnical(string $name): ?CategorizationResult
|
||||
{
|
||||
$publishers = 'Apress|Addison[._ -]Wesley|Manning|No[._ -]Starch|OReilly|Packt|Pragmatic|Wiley|Wrox';
|
||||
if (preg_match('/\b(' . $publishers . ')\b/i', $name)) return $this->matched(Category::BOOKS_TECHNICAL, 0.9, 'technical_publisher');
|
||||
$subjects = 'Programming|Python|JavaScript|Java|Database|Linux|DevOps|Machine[._ -]Learning|Data[._ -]Science';
|
||||
if (preg_match('/\b(' . $subjects . ')\b/i', $name) && preg_match('/\b(Book|Guide|Tutorial|Learn)\b/i', $name)) {
|
||||
return $this->matched(Category::BOOKS_TECHNICAL, 0.85, 'technical_subject');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected function checkMagazine(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/[._ -](Monthly|Weekly|Annual|Quarterly|Issue)[._ -]/i', $name)) return $this->matched(Category::BOOKS_MAGAZINES, 0.9, 'magazine_frequency');
|
||||
$magazines = 'Forbes|Fortune|GQ|National[._ -]Geographic|Newsweek|Time|Vogue|Wired|PC[._ -]Gamer';
|
||||
if (preg_match('/\b(' . $magazines . ')\b/i', $name)) return $this->matched(Category::BOOKS_MAGAZINES, 0.85, 'magazine_title');
|
||||
return null;
|
||||
}
|
||||
protected function checkEbook(string $name): ?CategorizationResult
|
||||
{
|
||||
$formats = 'EPUB|MOBI|AZW\d?|PDF|FB2|DJVU|LIT';
|
||||
if (preg_match('/\.(' . $formats . ')$/i', $name)) return $this->matched(Category::BOOKS_EBOOK, 0.9, 'ebook_format');
|
||||
if (preg_match('/\b(' . $formats . ')\b/i', $name)) return $this->matched(Category::BOOKS_EBOOK, 0.85, 'ebook_indicator');
|
||||
if (preg_match('/\b(E-?book|Kindle|Kobo|Nook)\b/i', $name)) return $this->matched(Category::BOOKS_EBOOK, 0.8, 'ebook_platform');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
class ConsoleCategorizer extends AbstractCategorizer
|
||||
{
|
||||
protected int $priority = 35;
|
||||
public function getName(): string { return 'Console'; }
|
||||
public function shouldSkip(ReleaseContext $context): bool {
|
||||
if ($context->hasAdultMarkers()) return true;
|
||||
// Skip TV shows (season patterns)
|
||||
if (preg_match('/[._ -]S\d{1,3}[._ -]?(E\d|Complete|Full|1080|720|480|2160|WEB|HDTV|BluRay)/i', $context->releaseName)) return true;
|
||||
return false;
|
||||
}
|
||||
public function categorize(ReleaseContext $context): CategorizationResult
|
||||
{
|
||||
$name = $context->releaseName;
|
||||
if ($result = $this->checkPS4($name)) return $result;
|
||||
if ($result = $this->checkPS3($name)) return $result;
|
||||
if ($result = $this->checkPSVita($name)) return $result;
|
||||
if ($result = $this->checkPSP($name)) return $result;
|
||||
if ($result = $this->checkXboxOne($name)) return $result;
|
||||
if ($result = $this->checkXbox360($name)) return $result;
|
||||
if ($result = $this->checkXbox($name)) return $result;
|
||||
if ($result = $this->checkWiiU($name)) return $result;
|
||||
if ($result = $this->checkWii($name)) return $result;
|
||||
if ($result = $this->check3DS($name)) return $result;
|
||||
if ($result = $this->checkNDS($name)) return $result;
|
||||
if ($result = $this->checkOther($name)) return $result;
|
||||
return $this->noMatch();
|
||||
}
|
||||
protected function checkPS4(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/^PS4[_\.\-]/i', $name) || preg_match('/CUSA\d{5}/i', $name) ||
|
||||
preg_match('/\.PS4-DUPLEX$/i', $name) || preg_match('/\bPS4\b|PlayStation\s*4/i', $name)) {
|
||||
return $this->matched(Category::GAME_PS4, 0.9, 'ps4');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected function checkPS3(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\bPS3\b|PlayStation\s*3/i', $name)) return $this->matched(Category::GAME_PS3, 0.9, 'ps3');
|
||||
return null;
|
||||
}
|
||||
protected function checkPSVita(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\bPS\s?Vita\b|PSV(ita)?\b/i', $name)) return $this->matched(Category::GAME_PSVITA, 0.9, 'psvita');
|
||||
return null;
|
||||
}
|
||||
protected function checkPSP(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\bPSP\b|PlayStation\s*Portable/i', $name)) return $this->matched(Category::GAME_PSP, 0.9, 'psp');
|
||||
return null;
|
||||
}
|
||||
protected function checkXboxOne(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\b(XboxOne|XBOX\s*One|XBONE|XB1|Xbox\s*Series[._ -]?[SX]|XSX|XSS)\b/i', $name)) {
|
||||
return $this->matched(Category::GAME_XBOXONE, 0.9, 'xboxone');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected function checkXbox360(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\b(Xbox360|XBOX360|X360)\b/i', $name)) return $this->matched(Category::GAME_XBOX360, 0.9, 'xbox360');
|
||||
return null;
|
||||
}
|
||||
protected function checkXbox(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\bXBOX\b/i', $name) && !preg_match('/\b(XBOX\s?360|XBOX\s?ONE|Series)\b/i', $name)) {
|
||||
return $this->matched(Category::GAME_XBOX, 0.85, 'xbox');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected function checkWiiU(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\bWii\s*U\b|WiiU/i', $name)) return $this->matched(Category::GAME_WIIU, 0.9, 'wiiu');
|
||||
return null;
|
||||
}
|
||||
protected function checkWii(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\bWii\b/i', $name) && !preg_match('/WiiU/i', $name)) return $this->matched(Category::GAME_WII, 0.85, 'wii');
|
||||
return null;
|
||||
}
|
||||
protected function check3DS(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\b3DS\b|Nintendo\s*3DS/i', $name)) return $this->matched(Category::GAME_3DS, 0.9, '3ds');
|
||||
return null;
|
||||
}
|
||||
protected function checkNDS(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\bNDS\b|Nintendo\s*DS/i', $name)) return $this->matched(Category::GAME_NDS, 0.9, 'nds');
|
||||
return null;
|
||||
}
|
||||
protected function checkOther(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\b(PS[12X]|PS2|SNES|NES|SEGA|GB[AC]?|Dreamcast|Saturn|Atari|N64)\b/i', $name) &&
|
||||
preg_match('/\b(EUR|JP|JPN|NTSC|PAL|USA|ROM)\b/i', $name)) {
|
||||
return $this->matched(Category::GAME_OTHER, 0.8, 'retro_console');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
class GroupNameCategorizer extends AbstractCategorizer
|
||||
{
|
||||
protected int $priority = 5;
|
||||
public function getName(): string { return 'GroupName'; }
|
||||
public function categorize(ReleaseContext $context): CategorizationResult
|
||||
{
|
||||
$groupName = $context->groupName;
|
||||
if (empty($groupName)) return $this->noMatch();
|
||||
if (preg_match('/alt\.binaries\..*?(tv|hdtv|tvseries)/i', $groupName)) return $this->matched(Category::TV_OTHER, 0.6, 'group_tv');
|
||||
if (preg_match('/alt\.binaries\..*?(movies?|dvd|bluray|x264)/i', $groupName)) return $this->matched(Category::MOVIE_OTHER, 0.6, 'group_movie');
|
||||
if (preg_match('/alt\.binaries\..*?(erotica|pictures\.erotica|xxx)/i', $groupName)) return $this->matched(Category::XXX_OTHER, 0.7, 'group_xxx');
|
||||
if (preg_match('/alt\.binaries\..*?(sounds?|mp3|music|lossless)/i', $groupName)) return $this->matched(Category::MUSIC_OTHER, 0.6, 'group_music');
|
||||
if (preg_match('/alt\.binaries\..*?(games?|console|psx|nintendo)/i', $groupName)) return $this->matched(Category::GAME_OTHER, 0.6, 'group_game');
|
||||
if (preg_match('/alt\.binaries\..*?(warez|0day|apps?|software)/i', $groupName)) return $this->matched(Category::PC_0DAY, 0.6, 'group_pc');
|
||||
if (preg_match('/alt\.binaries\..*?(e-?book|ebook|comics?)/i', $groupName)) return $this->matched(Category::BOOKS_EBOOK, 0.6, 'group_book');
|
||||
return $this->noMatch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
|
||||
/**
|
||||
* Categorizer for miscellaneous content and hash detection.
|
||||
* This runs last as a fallback.
|
||||
*/
|
||||
class MiscCategorizer extends AbstractCategorizer
|
||||
{
|
||||
protected int $priority = 100; // Lowest priority - run last
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Misc';
|
||||
}
|
||||
|
||||
public function categorize(ReleaseContext $context): CategorizationResult
|
||||
{
|
||||
$name = $context->releaseName;
|
||||
|
||||
// Check for hash patterns first
|
||||
if ($result = $this->checkHash($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Check for archive formats
|
||||
if ($result = $this->checkArchive($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Check for dataset/dump patterns
|
||||
if ($result = $this->checkDataset($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Check for obfuscated/encoded patterns
|
||||
if ($result = $this->checkObfuscated($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->noMatch();
|
||||
}
|
||||
|
||||
protected function checkHash(string $name): ?CategorizationResult
|
||||
{
|
||||
// MD5 hash (32 hex characters)
|
||||
if (preg_match('/\b[a-f0-9]{32}\b/i', $name)) {
|
||||
return $this->matched(Category::OTHER_HASHED, 0.8, 'hash_md5');
|
||||
}
|
||||
|
||||
// SHA-1 hash (40 hex characters)
|
||||
if (preg_match('/\b[a-f0-9]{40}\b/i', $name)) {
|
||||
return $this->matched(Category::OTHER_HASHED, 0.85, 'hash_sha1');
|
||||
}
|
||||
|
||||
// SHA-256 hash (64 hex characters)
|
||||
if (preg_match('/\b[a-f0-9]{64}\b/i', $name)) {
|
||||
return $this->matched(Category::OTHER_HASHED, 0.9, 'hash_sha256');
|
||||
}
|
||||
|
||||
// Generic long hex hash
|
||||
if (preg_match('/\b[a-f0-9]{32,128}\b/i', $name)) {
|
||||
return $this->matched(Category::OTHER_HASHED, 0.75, 'hash_generic');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkArchive(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\.(zip|rar|7z|tar|gz|bz2|xz|tgz|tbz2|cab|iso|img|dmg|pkg|archive)$/i', $name)) {
|
||||
return $this->matched(Category::OTHER_MISC, 0.5, 'archive');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkDataset(string $name): ?CategorizationResult
|
||||
{
|
||||
// Dataset/dump patterns that aren't media
|
||||
if (preg_match('/\b(sql|csv|dump|backup|dataset|collection)\b/i', $name) &&
|
||||
!preg_match('/\b(movie|tv|show|audio|video|book|game)\b/i', $name)) {
|
||||
return $this->matched(Category::OTHER_MISC, 0.6, 'dataset');
|
||||
}
|
||||
|
||||
// Data leaks/dumps (be careful with these)
|
||||
if (preg_match('/\b(leak|breach|data|database)\b/i', $name) &&
|
||||
preg_match('/\b(dump|export|backup)\b/i', $name) &&
|
||||
!preg_match('/\b(movie|tv|show|audio|video|book|game)\b/i', $name)) {
|
||||
return $this->matched(Category::OTHER_MISC, 0.6, 'data_dump');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkObfuscated(string $name): ?CategorizationResult
|
||||
{
|
||||
// Release names consisting only of uppercase letters and numbers
|
||||
if (preg_match('/^[A-Z0-9]{15,}$/', $name)) {
|
||||
return $this->matched(Category::OTHER_HASHED, 0.7, 'obfuscated_uppercase');
|
||||
}
|
||||
|
||||
// Long alphanumeric strings without typical release patterns
|
||||
if (preg_match('/^[a-zA-Z0-9]{25,}$/', $name) &&
|
||||
!preg_match('/\b(19|20)\d{2}\b/', $name)) {
|
||||
return $this->matched(Category::OTHER_HASHED, 0.65, 'obfuscated_long');
|
||||
}
|
||||
|
||||
// Only punctuation and numbers with no clear structure
|
||||
if (preg_match('/^[^a-zA-Z]*[A-Z0-9\._\-]{5,}[^a-zA-Z]*$/', $name) &&
|
||||
!preg_match('/\.(mkv|avi|mp4|mp3|flac|pdf|epub|exe|iso)$/i', $name)) {
|
||||
return $this->matched(Category::OTHER_MISC, 0.5, 'obfuscated_pattern');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
|
||||
/**
|
||||
* Categorizer for Movie content including HD, SD, UHD, 3D, Blu-ray, DVD, etc.
|
||||
*/
|
||||
class MovieCategorizer extends AbstractCategorizer
|
||||
{
|
||||
protected int $priority = 25;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Movie';
|
||||
}
|
||||
|
||||
public function shouldSkip(ReleaseContext $context): bool
|
||||
{
|
||||
// Skip if this looks like adult content
|
||||
if ($context->hasAdultMarkers()) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Skip episode-only patterns (E01, E02) - common in anime
|
||||
if (preg_match('/[._ -]E\d{1,4}[._ -]/i', $context->releaseName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip known anime release groups
|
||||
if (preg_match('/[.\-_ ](URANiME|ANiHLS|HaiKU|ANiURL|SkyAnime|Erai-raws|LostYears|Vodes|SubsPlease|Judas|Ember|YuiSubs|ASW|Tsundere-Raws|Anime-Raws)[.\-_ ]?/i', $context->releaseName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function categorize(ReleaseContext $context): CategorizationResult
|
||||
{
|
||||
$name = $context->releaseName;
|
||||
|
||||
// Check if it looks like movie content
|
||||
if (!$this->looksLikeMovie($name)) {
|
||||
return $this->noMatch();
|
||||
}
|
||||
|
||||
// Try specific movie subcategories in order of specificity
|
||||
if ($context->categorizeForeign && ($result = $this->checkForeign($name))) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkX265($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkUHD($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->check3D($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkBluRay($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkDVD($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($context->catWebDL && ($result = $this->checkWebDL($name))) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkHD($name, $context->catWebDL)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkSD($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkOther($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->noMatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if release name looks like movie content.
|
||||
*/
|
||||
protected function looksLikeMovie(string $name): bool
|
||||
{
|
||||
return (bool) preg_match('/[._ -]AVC|[BH][DR]RIP|(Bluray|Blu-Ray)|BD[._ -]?(25|50)?|\bBR\b|Camrip|[._ -]\d{4}[._ -].+(720p|1080p|Cam|HDTS|2160p)|DIVX|[._ -]DVD[._ -]|DVD-?(5|9|R|Rip)|Untouched|VHSRip|XVID|[._ -](DTS|TVrip|webrip|WEBDL|WEB-DL)[._ -]|\b(2160)p\b.*\b(Netflix|Amazon|NF|AMZN|Disney)\b/i', $name);
|
||||
}
|
||||
|
||||
protected function checkForeign(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/(danish|flemish|Deutsch|dutch|french|german|heb|hebrew|nl[._ -]?sub|dub(bed|s)?|\.NL|norwegian|swedish|swesub|spanish|Staffel)[._ -]|\(german\)|Multisub/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_FOREIGN, 0.8, 'foreign_language');
|
||||
}
|
||||
|
||||
if (stripos($name, 'Castellano') !== false) {
|
||||
return $this->matched(Category::MOVIE_FOREIGN, 0.8, 'foreign_castellano');
|
||||
}
|
||||
|
||||
if (preg_match('/(720p|1080p|AC3|AVC|DIVX|DVD(5|9|RIP|R)|XVID)[._ -](Dutch|French|German|ITA)|\(?(Dutch|French|German|ITA)\)?[._ -](720P|1080p|AC3|AVC|DIVX|DVD(5|9|RIP|R)|WEB(-DL|-?RIP)|HD[._ -]|XVID)/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_FOREIGN, 0.85, 'foreign_pattern');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkX265(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/(\w+[\.-_\s]+).*(x265).*(Tigole|SESKAPiLE|CHD|IAMABLE|THREESOME|OohLaLa|DEFLATE|NCmt)/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_X265, 0.9, 'x265_group');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkUHD(string $name): ?CategorizationResult
|
||||
{
|
||||
// Skip TV shows
|
||||
if (preg_match('/(S\d+).*(2160p).*(Netflix|Amazon|NF|AMZN).*(TrollUHD|NTb|VLAD|DEFLATE|CMRG)/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for UHD indicators
|
||||
if (stripos($name, '2160p') !== false ||
|
||||
preg_match('/\b(UHD|Ultra[._ -]HD|4K)\b/i', $name) ||
|
||||
(preg_match('/\b(HDR|HDR10|HDR10\+|Dolby[._ -]?Vision)\b/i', $name) &&
|
||||
preg_match('/\b(HEVC|H\.?265|x265)\b/i', $name)) ||
|
||||
(stripos($name, 'UHD') !== false &&
|
||||
preg_match('/\b(BR|BluRay|Blu[._ -]?Ray)\b/i', $name))) {
|
||||
return $this->matched(Category::MOVIE_UHD, 0.9, 'uhd');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function check3D(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/[._ -]3D\s?[\.\-_\[ ](1080p|(19|20)\d\d|AVC|BD(25|50)|Blu[._ -]?ray|CEE|Complete|GER|MVC|MULTi|SBS|H(-)?SBS)[._ -]/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_3D, 0.9, '3d');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkBluRay(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/bluray-|[._ -]bd?[._ -]?(25|50)|blu-ray|Bluray\s-\sUntouched|[._ -]untouched[._ -]/i', $name) &&
|
||||
!preg_match('/SecretUsenet\.com$/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_BLURAY, 0.9, 'bluray');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkDVD(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/(dvd\-?r|[._ -]dvd|dvd9|dvd5|[._ -]r5)[._ -]/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_DVD, 0.85, 'dvd');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkWebDL(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/web[._ -]dl|web-?rip/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_WEBDL, 0.85, 'webdl');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkHD(string $name, bool $catWebDL): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/720p|1080p|AVC|VC1|VC-1|web-dl|wmvhd|x264|XvidHD|bdrip/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_HD, 0.85, 'hd');
|
||||
}
|
||||
|
||||
if (!$catWebDL && preg_match('/web[._ -]dl|web-?rip/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_HD, 0.8, 'hd_webdl_fallback');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkSD(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/(divx|dvdscr|extrascene|dvdrip|\.CAM|HDTS(-LINE)?|vhsrip|xvid(vd)?)[._ -]/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_SD, 0.8, 'sd');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkOther(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/[._ -]cam[._ -]/i', $name)) {
|
||||
return $this->matched(Category::MOVIE_OTHER, 0.6, 'cam');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
|
||||
/**
|
||||
* Categorizer for Music content (MP3, Lossless, Video, Audiobook, Podcast).
|
||||
*/
|
||||
class MusicCategorizer extends AbstractCategorizer
|
||||
{
|
||||
protected int $priority = 40;
|
||||
|
||||
// Language patterns for foreign music
|
||||
protected const FOREIGN_LANGUAGES = 'arabic|brazilian|bulgarian|cantonese|chinese|croatian|czech|danish|deutsch|dutch|estonian|finnish|flemish|french|german|greek|hebrew|hungarian|icelandic|indian|iranian|italian|japanese|korean|latin|latvian|lithuanian|macedonian|mandarin|nordic|norwegian|persian|polish|portuguese|romanian|russian|serbian|slovenian|spanish|spanisch|swedish|thai|turkish|ukrainian|vietnamese';
|
||||
|
||||
protected const LANGUAGE_CODES = 'ar|bg|bl|cs|cz|da|de|dk|el|es|et|fi|fr|ger|gr|heb|hr|hu|hun|is|it|ita|jp|jap|ko|kor|lt|lv|mk|nl|no|pl|pt|ro|rs|ru|se|sk|sl|sr|sv|th|tr|ua|vi|zh';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Music';
|
||||
}
|
||||
|
||||
public function shouldSkip(ReleaseContext $context): bool
|
||||
{
|
||||
if ($context->hasAdultMarkers()) return true;
|
||||
// Skip TV shows (season patterns)
|
||||
if (preg_match('/[._ -]S\d{1,3}[._ -]?(E\d|Complete|Full|1080|720|480|2160|WEB|HDTV|BluRay)/i', $context->releaseName)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public function categorize(ReleaseContext $context): CategorizationResult
|
||||
{
|
||||
$name = $context->releaseName;
|
||||
|
||||
// Try each music category
|
||||
if ($result = $this->checkAudiobook($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkPodcast($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkMusicVideo($name, $context->categorizeForeign)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkLossless($name, $context->categorizeForeign)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkMP3($name, $context->categorizeForeign)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkOther($name, $context->categorizeForeign)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->noMatch();
|
||||
}
|
||||
|
||||
protected function checkForeign(string $name): bool
|
||||
{
|
||||
return (bool) preg_match('/(?:^|[\s\.\-_])(?:' . self::FOREIGN_LANGUAGES . '|' . self::LANGUAGE_CODES . ')(?:$|[\s\.\-_])/i', $name);
|
||||
}
|
||||
|
||||
protected function checkAudiobook(string $name): ?CategorizationResult
|
||||
{
|
||||
// Explicit audiobook indicators
|
||||
if (preg_match('/(?:^|[^a-zA-Z0-9])(?:Audiobook|Audio\s*Book|Talking\s*Book|ABEE|Audible)/i', $name)) {
|
||||
if (preg_match('/\b(?:Unabridged|Abridged|Narrated|Narrator|MP3|M4A|M4B|AAC|Read\s+By|Tantor|Blackstone|Brilliance|GraphicAudio|Penguin|Audible)\b/i', $name) ||
|
||||
preg_match('/\d+\s*CDs|\d+\s*Hours|Spoken\s+Word/i', $name) ||
|
||||
preg_match('/\.(mp3|m4a|m4b|aac|flac|ogg|wma)$/i', $name)) {
|
||||
return $this->matched(Category::MUSIC_AUDIOBOOK, 0.95, 'audiobook');
|
||||
}
|
||||
}
|
||||
|
||||
// Audiobook patterns
|
||||
if (preg_match('/(?:[\(_\[])(?:Audiobook|AB|Unabridged)(?:[\)_\]])/i', $name) ||
|
||||
preg_match('/Read\s+By\s+[A-Z][a-z]+\s+[A-Z][a-z]+/i', $name)) {
|
||||
return $this->matched(Category::MUSIC_AUDIOBOOK, 0.9, 'audiobook_pattern');
|
||||
}
|
||||
|
||||
// Legacy pattern
|
||||
if (preg_match('/(Audiobook|Audio.?Book)/i', $name)) {
|
||||
return $this->matched(Category::MUSIC_AUDIOBOOK, 0.85, 'audiobook_legacy');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkPodcast(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/(?:^|[^a-zA-Z0-9])(?:Podcast|Pod[._ -]?cast|Pod[._ -]Show)/i', $name)) {
|
||||
return $this->matched(Category::MUSIC_PODCAST, 0.9, 'podcast');
|
||||
}
|
||||
|
||||
// Known podcast networks with episode indicators
|
||||
if (preg_match('/\b(?:NPR|BBC[._ -]Sounds|Gimlet|Wondery|Stitcher|iHeart[._ -]?Radio|Joe[._ -]Rogan|RadioLab|Serial)\b/i', $name) &&
|
||||
preg_match('/\b(?:Podcast|Episode|EP?[._ -]?\d+|Show)\b/i', $name)) {
|
||||
return $this->matched(Category::MUSIC_PODCAST, 0.85, 'podcast_network');
|
||||
}
|
||||
|
||||
// Simple podcast match
|
||||
if (preg_match('/podcast/i', $name)) {
|
||||
return $this->matched(Category::MUSIC_PODCAST, 0.8, 'podcast_simple');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkMusicVideo(string $name, bool $categorizeForeign): ?CategorizationResult
|
||||
{
|
||||
// Music video indicators
|
||||
if (preg_match('/(?:^|[^a-zA-Z0-9])(?:Music\s*Video|Concert|Live\s*Show|Tour|Festival|MV|MTV)|\b(?:MVID|MVid)\b/i', $name)) {
|
||||
if (preg_match('/\b(?:720p|1080[pi]|2160p|BDRip|BluRay|DVDRip|HDTV|WebRip|WEB-DL|x264|x265)\b/i', $name) ||
|
||||
preg_match('/\b(?:Live|Unplugged|Acoustic|World\s*Tour|in\s*Concert|Official\s*Video|Bootleg|Remastered)\b/i', $name) ||
|
||||
preg_match('/\.(mkv|mp4|avi|ts|m2ts|mpg|mpeg|mov|wmv|vob|m4v)$/i', $name)) {
|
||||
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.85, 'music_video_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_VIDEO, 0.9, 'music_video');
|
||||
}
|
||||
}
|
||||
|
||||
// Artist-title pattern with video format
|
||||
if (preg_match('/^[A-Z0-9][A-Za-z0-9\.\s\&\'\(\)\-]+\s+\-\s+[A-Z0-9][A-Za-z0-9\.\s\&\'\(\)\-]+.*?\b(720p|1080[pi]|2160p|Bluray|x264|x265)\b/i', $name)) {
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.8, 'music_video_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_VIDEO, 0.8, 'music_video_artist');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkLossless(string $name, bool $categorizeForeign): ?CategorizationResult
|
||||
{
|
||||
// Lossless format indicators
|
||||
if (preg_match('/(?:^|[^a-zA-Z0-9])(?:FLAC|APE|WAV|ALAC|DSD|DSF|AIFF|PCM|Lossless)|\b(?:FLAC|APE|WAV|ALAC|DSD|DSF|AIFF|PCM)\b/i', $name)) {
|
||||
if (preg_match('/\b(?:24[Bb]it|96kHz|192kHz|Hi[- ]?Res|HD[- ]?Tracks|Vinyl[- ]?Rip|CD[- ]?Rip|WEB[- ]?Rip|HDtracks|Qobuz|Tidal|MQA|SACD)\b/i', $name) ||
|
||||
preg_match('/\.(flac|ape|wav|aiff|dsf|dff|m4a|tak)$/i', $name)) {
|
||||
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.9, 'lossless_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_LOSSLESS, 0.9, 'lossless');
|
||||
}
|
||||
}
|
||||
|
||||
// FLAC patterns
|
||||
if (preg_match('/\[(19|20)\d\d\][._ -]\[FLAC\]|([\(\[])flac([\)\]])|FLAC\-(19|20)\d\d\-[a-z0-9]{1,12}|\.flac"|(19|20)\d\d\sFLAC|[._ -]FLAC.+(19|20)\d\d[._ -]| FLAC$/i', $name) ||
|
||||
preg_match('/\d{3,4}kbps[._ -]FLAC|\[FLAC\]|\(FLAC\)|FLACME|FLAC[._ -]\d{3,4}(kbps)?|WEB[._ -]FLAC/i', $name)) {
|
||||
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.85, 'flac_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_LOSSLESS, 0.85, 'flac');
|
||||
}
|
||||
|
||||
// Other lossless formats
|
||||
if (preg_match('/\b(?:APE|Monkey\'s[._ -]Audio|WavPack|WV|TAK|TTA|ALAC|Apple[._ -]Lossless)\b|\.(ape|wv|tak|tta)$/i', $name)) {
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.85, 'lossless_format_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_LOSSLESS, 0.85, 'lossless_format');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkMP3(string $name, bool $categorizeForeign): ?CategorizationResult
|
||||
{
|
||||
// MP3 indicators
|
||||
if (preg_match('/(?:^|[^a-zA-Z0-9])(?:MP3|320kbps|256kbps|192kbps|128kbps|CBR|VBR)|\b(?:MP3)\b|[\._-](?:MP3)[\._-]|\.mp3$/i', $name)) {
|
||||
if (preg_match('/\b(?:320|256|192|128)[._-]?kbps|\b(?:320|256|192|128)[._-]?K|\((?:320|256|192|128)\)|\[(?:320|256|192|128)\]|V0|V2|VBR/i', $name) ||
|
||||
preg_match('/\b(?:CD[._-]?Rip|Web[._-]?Rip|WEB|iTunes|AmazonRip|Spotify[._-]?Rip|MP3\s*\-\s*\d{3}kbps)\b/i', $name) ||
|
||||
preg_match('/\.(m3u|mp3)"|rip(?:192|256|320)|[._-]FM[._-].+MP3/i', $name)) {
|
||||
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.85, 'mp3_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_MP3, 0.85, 'mp3');
|
||||
}
|
||||
}
|
||||
|
||||
// MP3 scene patterns
|
||||
if (preg_match('/^[a-zA-Z0-9]{1,12}[._-](19|20)\d\d[._-][a-zA-Z0-9]{1,12}$|[a-z0-9]{1,12}\-(19|20)\d\d\-[a-z0-9]{1,12}/i', $name)) {
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.75, 'mp3_scene_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_MP3, 0.75, 'mp3_scene');
|
||||
}
|
||||
|
||||
// Bitrate patterns
|
||||
if (preg_match('/[\.\-\(\[_ ]\d{2,3}k[\.\-\)\]_ ]|\((192|256|320)\)|(320|cd|eac|vbr)[._-]+mp3|(cd|eac|mp3|vbr)[._-]+320/i', $name)) {
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.8, 'mp3_bitrate_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_MP3, 0.8, 'mp3_bitrate');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkOther(string $name, bool $categorizeForeign): ?CategorizationResult
|
||||
{
|
||||
// Compilation and VA indicators
|
||||
if (preg_match('/(?:^|[^a-zA-Z0-9])(?:Compilation|Various[._ -]Artists|OST|Soundtrack|B-Sides|Greatest[._ -]Hits|Anthology)|\b(?:VA|V\.A|Bonus[._ -]Track|Discography|Box[._ -]Set)\b/i', $name)) {
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.8, 'music_other_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_OTHER, 0.8, 'music_other');
|
||||
}
|
||||
|
||||
// Album/CD patterns
|
||||
if (preg_match('/(?:\d)[._ -](?:CD|Albums|LP)[._ -](?:Set|Compilation)|CD[._ -](Collection|Box|SET)|(\d)-?CD[._ -]/i', $name) ||
|
||||
preg_match('/Vinyl[._ -](?:24[._ -]96|2496|Collection|RIP)|WEB[._ -](?:Single|Album)|EP[._ -]\d{4}|\bEP\b.+(?:19|20)\d\d|Live[._ -](?:at|At|@)/i', $name)) {
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.75, 'music_album_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_OTHER, 0.75, 'music_album');
|
||||
}
|
||||
|
||||
// DJ mixes and labels
|
||||
if (preg_match('/\b(?:Ministry[._ -]of[._ -]Sound|Hed[._ -]Kandi|Cream|Fabric[._ -]Live|Ultra[._ -]Music)\b/i', $name) ||
|
||||
preg_match('/\b(?:DJ[._ -]Mix|Mixed[._ -]By|Tiesto[._ -]Club|Radio[._ -]Show|Club[._ -]Hits)\b/i', $name)) {
|
||||
if ($categorizeForeign && $this->checkForeign($name)) {
|
||||
return $this->matched(Category::MUSIC_FOREIGN, 0.75, 'music_dj_foreign');
|
||||
}
|
||||
return $this->matched(Category::MUSIC_OTHER, 0.75, 'music_dj');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
|
||||
/**
|
||||
* Categorizer for PC content (Games, Software, ISO, 0day, Mac, Phone apps).
|
||||
*/
|
||||
class PcCategorizer extends AbstractCategorizer
|
||||
{
|
||||
protected int $priority = 30;
|
||||
|
||||
// Common PC game release groups
|
||||
protected const PC_GROUPS = '0x0007|ALiAS|ANOMALY|BACKLASH|BAT|CODEX|CPY|DARKS(?:iDERS|IDERS)|DEViANCE|DOGE|DODI|ELAMIGOS|EMPRESS|FITGIRL|FAS(?:DOX|iSO)|FLT|GOG(?:-GAMES)?|GOLDBERG|HI2U|HOODLUM|INLAWS|JAGUAR|MAZE|MONEY|OUTLAWS|PLAZA|PROPHET|RAZOR1911|RAiN|RELOADED|RUNE|SiMPLEX|SKIDROW|TENOKE|TiNYiSO|UNLEASHED|P2P';
|
||||
|
||||
// PC-only keywords
|
||||
protected const PC_KEYWORDS = 'PC[ _.-]?GAMES?|\[(?:PC)\]|\(PC\)|Steam(?:[ ._-]?Rip|\b)|GOG(?:\b|[ ._-])|Retail\s*PC|DRM-?Free|Win(All|32|64)\b|Windows(?:\s?10|\s?11)?\b|Repack';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'PC';
|
||||
}
|
||||
|
||||
public function shouldSkip(ReleaseContext $context): bool
|
||||
{
|
||||
if ($context->hasAdultMarkers()) return true;
|
||||
// Skip TV shows (season patterns)
|
||||
if (preg_match('/[._ -]S\d{1,3}[._ -]?(E\d|Complete|Full|1080|720|480|2160|WEB|HDTV|BluRay)/i', $context->releaseName)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public function categorize(ReleaseContext $context): CategorizationResult
|
||||
{
|
||||
$name = $context->releaseName;
|
||||
|
||||
// Try each PC category
|
||||
if ($result = $this->checkPhone($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkMac($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkPCGame($name, $context->poster)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkISO($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->check0day($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->noMatch();
|
||||
}
|
||||
|
||||
protected function checkPhone(string $name): ?CategorizationResult
|
||||
{
|
||||
// iOS
|
||||
if (preg_match('/[^a-z0-9](IPHONE|ITOUCH|IPAD)[._ -]/i', $name)) {
|
||||
return $this->matched(Category::PC_PHONE_IOS, 0.9, 'ios');
|
||||
}
|
||||
|
||||
// Android
|
||||
if (preg_match('/[._ -]?(ANDROID)[._ -]/i', $name)) {
|
||||
return $this->matched(Category::PC_PHONE_ANDROID, 0.9, 'android');
|
||||
}
|
||||
|
||||
// Other mobile platforms
|
||||
if (preg_match('/[^a-z0-9](symbian|xscale|wm5|wm6)[._ -]/i', $name)) {
|
||||
return $this->matched(Category::PC_PHONE_OTHER, 0.85, 'phone_other');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkMac(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/(\b|[._ -])mac([\.\s])?osx(\b|[\-_. ])/i', $name)) {
|
||||
return $this->matched(Category::PC_MAC, 0.9, 'mac');
|
||||
}
|
||||
|
||||
if (preg_match('/\b(Mac\s?OS\s?X|macOS)\b/i', $name)) {
|
||||
return $this->matched(Category::PC_MAC, 0.9, 'macos');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkPCGame(string $name, string $poster): ?CategorizationResult
|
||||
{
|
||||
// Exclude console releases
|
||||
$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';
|
||||
if (preg_match($consoleOrMac, $name) || preg_match('/\b(Mac\s?OS\s?X|macOS)\b/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Exclude TV shows
|
||||
$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';
|
||||
if (preg_match($tvPatterns, $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for PC game patterns
|
||||
$pattern = '/(?:(?:^|[\s\._-])(?:' . self::PC_GROUPS . ')(?:$|[\s\._-])|' . self::PC_KEYWORDS . ')/i';
|
||||
|
||||
if (preg_match($pattern, $name)) {
|
||||
return $this->matched(Category::PC_GAMES, 0.9, 'pc_game');
|
||||
}
|
||||
|
||||
// Check poster
|
||||
if (preg_match('/<PC@MASTER\.RACE>/i', $poster)) {
|
||||
return $this->matched(Category::PC_GAMES, 0.85, 'pc_game_poster');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkISO(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/[._ -]([a-zA-Z]{2,10})?iso[ _.-]|[\-. ]([a-z]{2,10})?iso$/i', $name)) {
|
||||
return $this->matched(Category::PC_ISO, 0.85, 'iso');
|
||||
}
|
||||
|
||||
// Training/Tutorial ISOs
|
||||
if (preg_match('/[._ -](DYNAMiCS|INFINITESKILLS|UDEMY|kEISO|PLURALSIGHT|DIGITALTUTORS|TUTSPLUS|OSTraining|PRODEV|CBT\.Nuggets|COMPRISED)/i', $name)) {
|
||||
return $this->matched(Category::PC_ISO, 0.9, 'training_iso');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function check0day(string $name): ?CategorizationResult
|
||||
{
|
||||
// Explicit 0day indicators
|
||||
if (preg_match('/[._ -]exe$|[._ -](utorrent|Virtualbox)[._ -]|\b0DAY\b|incl.+crack| DRM$|>DRM</i', $name)) {
|
||||
return $this->matched(Category::PC_0DAY, 0.9, '0day_explicit');
|
||||
}
|
||||
|
||||
// System/architecture indicators
|
||||
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)) {
|
||||
return $this->matched(Category::PC_0DAY, 0.85, '0day_system');
|
||||
}
|
||||
|
||||
// Software vendors and patterns
|
||||
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)) {
|
||||
return $this->matched(Category::PC_0DAY, 0.85, '0day_software');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
|
||||
/**
|
||||
* Categorizer for TV content including HD, SD, UHD, Anime, Sports, Documentaries, etc.
|
||||
*/
|
||||
class TvCategorizer extends AbstractCategorizer
|
||||
{
|
||||
protected int $priority = 20;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'TV';
|
||||
}
|
||||
|
||||
public function shouldSkip(ReleaseContext $context): bool
|
||||
{
|
||||
return $context->hasAdultMarkers();
|
||||
}
|
||||
|
||||
public function categorize(ReleaseContext $context): CategorizationResult
|
||||
{
|
||||
$name = $context->releaseName;
|
||||
|
||||
if (!$this->looksLikeTV($name)) {
|
||||
return $this->noMatch();
|
||||
}
|
||||
|
||||
if ($result = $this->checkAnime($name)) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $this->checkSport($name)) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $this->checkDocumentary($name)) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $this->checkForeign($context)) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $this->checkX265($name)) {
|
||||
return $result;
|
||||
}
|
||||
if ($context->catWebDL && ($result = $this->checkWebDL($name))) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $this->checkUHD($name)) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $this->checkHD($name, $context->catWebDL)) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $this->checkSD($name)) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $this->checkOther($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->noMatch();
|
||||
}
|
||||
|
||||
protected function looksLikeTV(string $name): bool
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
// Episode-only pattern: .E01., .E02., E01.1080p (common in anime)
|
||||
if (preg_match('/[._ -]E\d{1,4}[._ -]/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
// Season pack with Complete/Full: S01.Complete, S01.Full
|
||||
if (preg_match('/[._ -]S\d{1,3}[._ -]?(Complete|COMPLETE|Full|FULL)/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
// Season pack with resolution/quality: S01.1080p, S01.720p, S01.2160p, S02.WEB-DL
|
||||
if (preg_match('/[._ -]S\d{1,3}[._ -](480p|720p|1080[pi]|2160p|4K|UHD|WEB|HDTV|BluRay|NF|AMZN|DSNP|ATVP|HMAX)/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
// Episode pattern: Episode 01, Ep.01, Ep 1
|
||||
if (preg_match('/\b(Episode|Ep)[._ -]?\d{1,4}\b/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
// TV source markers
|
||||
if (preg_match('/\b(HDTV|PDTV|DSR|TVRip|SATRip|DTHRip)\b/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
// Daily show pattern: Show.Name.2024.01.15 or Show.Name.2024-01-15
|
||||
if (preg_match('/[._ -](19|20)\d{2}[._ -]\d{2}[._ -]\d{2}[._ -]/i', $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)[.\-_ ]?/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function checkAnime(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/[._ -]Anime[._ -]/i', $name)) {
|
||||
return $this->matched(Category::TV_ANIME, 0.95, 'anime_pattern');
|
||||
}
|
||||
// Known anime release groups - now matches anywhere in the name, not just at the end
|
||||
if (preg_match('/[.\-_ ](URANiME|ANiHLS|HaiKU|ANiURL|SkyAnime|Erai-raws|LostYears|Vodes|SubsPlease|Judas|Ember|EMBER|YuiSubs|ASW|Tsundere-Raws|Anime-Raws)[.\-_ ]?/i', $name)) {
|
||||
return $this->matched(Category::TV_ANIME, 0.95, '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');
|
||||
}
|
||||
// Episode pattern with known anime indicators
|
||||
if (preg_match('/[._ -]E\d{1,4}[._ -]/i', $name) &&
|
||||
preg_match('/\b(BluRay|BD|BDRip)\b/i', $name) &&
|
||||
!preg_match('/\bS\d{1,3}\b/i', $name)) {
|
||||
// Episode-only pattern with BluRay but no season - likely anime
|
||||
return $this->matched(Category::TV_ANIME, 0.8, 'anime_episode_bluray');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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)) {
|
||||
return $this->matched(Category::TV_SPORT, 0.85, 'sport');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkDocumentary(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/\b(Documentary|Docu[._ -]?Series|DOCU)\b/i', $name)) {
|
||||
return $this->matched(Category::TV_DOCU, 0.85, 'documentary');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkForeign(ReleaseContext $context): ?CategorizationResult
|
||||
{
|
||||
if (!$context->categorizeForeign) {
|
||||
return null;
|
||||
}
|
||||
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)) {
|
||||
return $this->matched(Category::TV_FOREIGN, 0.8, 'foreign_language');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkX265(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/(S\d+).*(x265).*(rmteam|MeGusta|HETeam|PSA|ONLY|H4S5S|TrollHD|ImE)/i', $name)) {
|
||||
return $this->matched(Category::TV_X265, 0.9, 'x265_group');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkWebDL(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/web[._ -]dl|web-?rip/i', $name)) {
|
||||
return $this->matched(Category::TV_WEBDL, 0.85, 'webdl');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkUHD(string $name): ?CategorizationResult
|
||||
{
|
||||
// Single episode UHD
|
||||
if (preg_match('/S\d+[._ -]?E\d+/i', $name) && preg_match('/2160p/i', $name)) {
|
||||
return $this->matched(Category::TV_UHD, 0.9, 'uhd_episode');
|
||||
}
|
||||
// Season pack UHD
|
||||
if (preg_match('/[._ -]S\d+[._ -].*2160p/i', $name)) {
|
||||
return $this->matched(Category::TV_UHD, 0.9, 'uhd_season');
|
||||
}
|
||||
// UHD with streaming service markers
|
||||
if (preg_match('/(S\d+).*(2160p).*(Netflix|Amazon|NF|AMZN).*(TrollUHD|NTb|VLAD|DEFLATE|POFUDUK|CMRG)/i', $name)) {
|
||||
return $this->matched(Category::TV_UHD, 0.9, 'uhd_streaming');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkHD(string $name, bool $catWebDL): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/1080([ip])|720p|bluray/i', $name)) {
|
||||
return $this->matched(Category::TV_HD, 0.85, 'hd_resolution');
|
||||
}
|
||||
if (!$catWebDL && preg_match('/web[._ -]dl|web-?rip/i', $name)) {
|
||||
return $this->matched(Category::TV_HD, 0.8, 'hd_webdl_fallback');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkSD(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/(360|480|576)p|Complete[._ -]Season|dvdr(ip)?|dvd5|dvd9|\.pdtv|SD[._ -]TV|TVRip|NTSC|BDRip|hdtv|xvid/i', $name)) {
|
||||
return $this->matched(Category::TV_SD, 0.8, 'sd_format');
|
||||
}
|
||||
if (preg_match('/(([HP])D[._ -]?TV|DSR|WebRip)[._ -]x264/i', $name)) {
|
||||
return $this->matched(Category::TV_SD, 0.8, 'sd_codec');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkOther(string $name): ?CategorizationResult
|
||||
{
|
||||
// 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)) {
|
||||
return $this->matched(Category::TV_OTHER, 0.6, 'tv_season_pack');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization\Categorizers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
|
||||
/**
|
||||
* Categorizer for Adult/XXX content.
|
||||
*/
|
||||
class XxxCategorizer extends AbstractCategorizer
|
||||
{
|
||||
protected int $priority = 10; // High priority - should run early
|
||||
|
||||
// Known adult studios/sites - comprehensive list
|
||||
protected const KNOWN_STUDIOS = 'Brazzers|NaughtyAmerica|RealityKings|Bangbros|BangBros18|TeenFidelity|PornPros|SexArt|WowGirls|Vixen|Blacked|Tushy|Deeper|Bellesa|Defloration|MetArt|MetArtX|TheLifeErotic|VivThomas|JoyMii|Nubiles|NubileFilms|FamilyStrokes|X-Art|Babes|Twistys|WetAndPuffy|WowPorn|MomsTeachSex|Mofos|BangBus|Passion-HD|EvilAngel|DorcelClub|Private|Hustler|CherryPimps|PureTaboo|LadyLyne|TeamSkeet|GirlsWay|SweetSinner|NewSensations|Digital[._ -]?Playground|Wicked|Penthouse|Playboy|Kink|HardX|ArchAngel|JulesJordan|ManuelFerrara|LesbianX|AllAnal|DarkX|Elegant[._ -]?Angel|ZeroTolerance|Score|PornFidelity|Kelly[._ -]?Madison|DDF[._ -]?Network|21Sextury|21Naturals|Colette|SexMex|Bang|SpankBang|PornWorld|LegalPorno|AnalVids|GonzoXXX|RoccoSiffredi|Fake[._ -]?Hub|FakeAgent|FakeTaxi|FakeHostel|PublicAgent|StrandedTeens|Property[._ -]?Sex|Dane[._ -]?Jones|Lets[._ -]?Doe[._ -]?It|Office[._ -]?Obsession|SexyHub|Massage[._ -]?Rooms|Fitness[._ -]?Rooms|Female[._ -]?Agent|MissaX|All[._ -]?Girl[._ -]?Massage|Fantasy[._ -]?Massage|Nurumassage|Soapymassage|Reality[._ -]?Junkies|Perv[._ -]?Mom|Bad[._ -]?Milfs|Milf[._ -]?Body|Step[._ -]?Siblings|Sis[._ -]?Loves[._ -]?Me|Brother[._ -]??Crush|Dad[._ -]?Crush|Mom[._ -]?Knows[._ -]?Best|Bratty[._ -]?Sis|My[._ -]?Family[._ -]?Pies|Family[._ -]?Therapy|Nubiles[._ -]?Porn|Step[._ -]?Fantasy|Caught[._ -]?Fapping|She[._ -]?Will[._ -]?Cheat|Dirty[._ -]?Wives[._ -]?Club|Big[._ -]?Tits[._ -]?Round[._ -]?Asses|Ass[._ -]?Parade|Monsters[._ -]?Of[._ -]?Cock|Brown[._ -]?Bunnies|Teens[._ -]?Love[._ -]?Huge[._ -]?Cocks|Ass[._ -]?Masterpiece|Bang[._ -]?Casting|Holed|Tiny4K|Lubed|POVD|Exotic4K|CastingCouch[._ -]?X|Casting[._ -]?Couch|Creampie[._ -]?Angels|Digital[._ -]?Desire|Femjoy|Hegre|Joymii|Met[._ -]?Art|MPL[._ -]?Studios|Rylsky[._ -]?Art|Showy[._ -]?Beauty|Stunning18|Photodromm|Watch4Beauty|Wow[._ -]?Girls|Yonitale';
|
||||
|
||||
// Adult keywords
|
||||
protected const ADULT_KEYWORDS = 'Anal|Ass|BBW|BDSM|Blow|Boob|Bukkake|Casting|Couch|Cock|Compilation|Creampie|Cum|Dick|Dildo|Facial|Fetish|Fuck|Gang|Hardcore|Homemade|Horny|Interracial|Lesbian|MILF|Masturbat|Nympho|Oral|Orgasm|Penetrat|Pornstar|POV|Pussy|Riding|Seduct|Sex|Shaved|Slut|Squirt|Suck|Swallow|Threesome|Tits|Titty|Toy|Virgin|Whore';
|
||||
|
||||
// VR sites
|
||||
protected const VR_SITES = 'SexBabesVR|LittleCapriceVR|VRoomed|VRMagic|TonightsGirlfriend|NaughtyAmericaVR|BaDoinkVR|WankzVR|VRBangers|StripzVR|RealJamVR|TmwVRnet|MilfVR|KinkVR|CzechVR(?:Fetish)?|HoloGirlsVR|WetVR|XSinsVR|VRCosplayX|BIBIVR|SLR|SexLikeReal';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'XXX';
|
||||
}
|
||||
|
||||
public function categorize(ReleaseContext $context): CategorizationResult
|
||||
{
|
||||
$name = $context->releaseName;
|
||||
|
||||
// Check if it looks like adult content
|
||||
if (!$this->looksLikeXxx($name)) {
|
||||
return $this->noMatch();
|
||||
}
|
||||
|
||||
// Try specific XXX subcategories in order of specificity
|
||||
if ($result = $this->checkOnlyFans($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkVR($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkUHD($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkClipHD($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkPack($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkClipSD($name, $context->poster)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkSD($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($context->catWebDL && ($result = $this->checkWebDL($name))) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkX264($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkXvid($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkImageset($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkWMV($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkDVD($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($result = $this->checkOther($name)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->noMatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if release name looks like adult content.
|
||||
*/
|
||||
protected function looksLikeXxx(string $name): bool
|
||||
{
|
||||
// Check for XXX marker
|
||||
if (preg_match('/\bXXX\b/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for known studios/sites
|
||||
if (preg_match('/\b(' . self::KNOWN_STUDIOS . ')\b/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for adult content indicators combined with video markers
|
||||
if (preg_match('/\b(' . self::ADULT_KEYWORDS . ')\b/i', $name) &&
|
||||
preg_match('/\b(720p|1080p|2160p|4k|mp4|mkv|avi|wmv)\b/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Site with date pattern: sitename.YYYY.MM.DD or sitename.YY.MM.DD
|
||||
// This pattern is very common for adult sites but rare for regular content
|
||||
if (preg_match('/^[A-Za-z]+[.\-_ ](19|20)?\d{2}[.\-_ ]\d{2}[.\-_ ]\d{2}[.\-_ ][A-Za-z]/i', $name)) {
|
||||
// Check it's not a TV daily show by checking for adult keywords or specific patterns
|
||||
if (preg_match('/\b(' . self::ADULT_KEYWORDS . ')\b/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
// Check for performer name patterns (firstname.lastname) after the date
|
||||
if (preg_match('/\d{2}[.\-_ ]([a-z]+)[.\-_ ]([a-z]+)[.\-_ ]/i', $name)) {
|
||||
// Has a "firstname.lastname" pattern after date - likely adult
|
||||
// But exclude obvious TV patterns
|
||||
if (!preg_match('/\b(S\d{1,2}E\d{1,2}|Episode|Season|HDTV|PDTV)\b/i', $name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function checkOnlyFans(string $name): ?CategorizationResult
|
||||
{
|
||||
// Skip photo packs unless there's a video hint
|
||||
if (preg_match('/\b(photo(set)?|image(set)?|pics?|wallpapers?|collection|pack)\b/i', $name) &&
|
||||
!preg_match('/\b(mp4|mkv|mov|wmv|avi|webm|h\.?264|x264|h\.?265|x265)\b/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/\bOnly[-_ ]?Fans\b|^OF\./i', $name)) {
|
||||
return $this->matched(Category::XXX_ONLYFANS, 0.95, 'onlyfans');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkVR(string $name): ?CategorizationResult
|
||||
{
|
||||
if (stripos($name, 'vr') === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Require either a VR site token or explicit VR180/VR360
|
||||
if (!preg_match('/\bVR(?:180|360)\b/i', $name) &&
|
||||
!preg_match('/\b(' . self::VR_SITES . ')\b/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// VR pattern matching
|
||||
$vrPattern = '/\b(' . self::VR_SITES . ')\b|\bVR(?:180|360)\b|\b(?:5K|6K|7K|8K)\b.*\bVR\b|\b(?:GearVR|Oculus|Quest[123]?|PSVR|Vive|Index|Pimax)\b/i';
|
||||
|
||||
if (preg_match($vrPattern, $name)) {
|
||||
// Verify XXX content
|
||||
if (preg_match('/\b(' . self::VR_SITES . ')\b/i', $name) || preg_match('/\bXXX\b/i', $name)) {
|
||||
return $this->matched(Category::XXX_VR, 0.95, 'vr');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkUHD(string $name): ?CategorizationResult
|
||||
{
|
||||
if (!preg_match('/\b(2160p|4k|UHD|Ultra[._ -]?HD)\b/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for adult markers
|
||||
$hasAdultMarker = preg_match('/\bXXX\b/i', $name) ||
|
||||
preg_match('/\b(' . self::KNOWN_STUDIOS . ')\b/i', strtolower($name)) ||
|
||||
preg_match('/\b(Hardcore|Porn|Sex|Anal|Creampie|MILF|Lesbian|Teen|Interracial)\b/i', $name);
|
||||
|
||||
if (!$hasAdultMarker) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Known UHD release groups
|
||||
if (preg_match('/XXX.+2160p[\w\-.]+M[PO][V4]-(KTR|GUSH|FaiLED|SEXORS|hUSHhUSH|YAPG|WRB|NBQ|FETiSH)/i', $name)) {
|
||||
return $this->matched(Category::XXX_UHD, 0.95, 'uhd_group');
|
||||
}
|
||||
|
||||
return $this->matched(Category::XXX_UHD, 0.9, 'uhd');
|
||||
}
|
||||
|
||||
protected function checkClipHD(string $name): ?CategorizationResult
|
||||
{
|
||||
// Exclude packs and collections
|
||||
if (preg_match('/^(Complete|Pack|Collection|Anthology|Siterip|SiteRip)\b/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Exclude TV shows
|
||||
if (preg_match('/\b(S\d{1,2}E\d{1,2}|S\d{1,2}|Season\s\d{1,2})\b/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for HD resolution
|
||||
$hasHD = preg_match('/\b(720p|1080p|2160p|HD|4K)\b/i', $name);
|
||||
|
||||
// Studio + performer + HD resolution
|
||||
if (preg_match('/^(' . self::KNOWN_STUDIOS . ')\.([A-Z][a-z]+).*?(720p|1080p|2160p|HD|4K)/i', $name)) {
|
||||
return $this->matched(Category::XXX_CLIPHD, 0.9, 'clip_hd_studio');
|
||||
}
|
||||
|
||||
// Known studio with date pattern: site.YYYY.MM.DD or site.YY.MM.DD
|
||||
if (preg_match('/^(' . self::KNOWN_STUDIOS . ')[.\-_ ](19|20)?\d{2}[.\-_ ]\d{2}[.\-_ ]\d{2}/i', $name)) {
|
||||
if ($hasHD) {
|
||||
return $this->matched(Category::XXX_CLIPHD, 0.95, 'clip_hd_studio_date');
|
||||
}
|
||||
// Even without HD marker, if it's a known studio with date pattern, likely XXX
|
||||
return $this->matched(Category::XXX_X264, 0.85, 'studio_date');
|
||||
}
|
||||
|
||||
// Date pattern with 4-digit year: site.YYYY.MM.DD.performer.title.1080p
|
||||
if (preg_match('/^([A-Z][a-zA-Z0-9]+)[.\-_ ](19|20)\d{2}[.\-_ ]\d{2}[.\-_ ]\d{2}[.\-_ ]/i', $name) &&
|
||||
!preg_match('/\b(S\d{2}E\d{2}|Documentary|Series)\b/i', $name)) {
|
||||
// Check if it has adult keywords or HD resolution
|
||||
if ($hasHD || preg_match('/\b(' . self::ADULT_KEYWORDS . ')\b/i', $name)) {
|
||||
return $this->matched(Category::XXX_CLIPHD, 0.85, 'clip_hd_date_4digit');
|
||||
}
|
||||
}
|
||||
|
||||
// Date pattern with 2-digit year: site.YY.MM.DD.performer.title.1080p
|
||||
if (preg_match('/^([A-Z][a-zA-Z0-9]+)\.(\d{2})\.(\d{2})\.(\d{2})\..*?(720p|1080p|2160p|HD|4K)/i', $name) &&
|
||||
!preg_match('/\b(S\d{2}E\d{2}|Documentary|Series)\b/i', $name)) {
|
||||
return $this->matched(Category::XXX_CLIPHD, 0.85, 'clip_hd_date');
|
||||
}
|
||||
|
||||
// XXX with HD resolution
|
||||
if (preg_match('/\b(XXX|MILF|Anal|Sex|Porn)[._ -]+(720p|1080p|2160p|HD|4K)\b/i', $name) ||
|
||||
preg_match('/\b(720p|1080p|2160p|HD|4K)[._ -]+(XXX|MILF|Anal|Sex|Porn)\b/i', $name)) {
|
||||
return $this->matched(Category::XXX_CLIPHD, 0.8, 'clip_hd_xxx');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkPack(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/[ .]PACK[ .]/i', $name)) {
|
||||
return $this->matched(Category::XXX_PACK, 0.85, 'pack');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkClipSD(string $name, string $poster): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/anon@y[.]com|@md-hobbys[.]com|oz@lot[.]com/i', $poster)) {
|
||||
return $this->matched(Category::XXX_CLIPSD, 0.85, 'clip_sd_poster');
|
||||
}
|
||||
|
||||
if (preg_match('/(iPT\sTeam|KLEENEX)/i', $name) || stripos($name, 'SDPORN') !== false) {
|
||||
return $this->matched(Category::XXX_CLIPSD, 0.85, 'clip_sd');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkSD(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/SDX264XXX|XXX\.HR\./i', $name)) {
|
||||
return $this->matched(Category::XXX_SD, 0.85, 'sd');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkWebDL(string $name): ?CategorizationResult
|
||||
{
|
||||
// Exclude TV shows
|
||||
if (preg_match('/\b(S\d{1,2}E\d{1,2})\b/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/web[._ -]dl|web-?rip/i', $name) &&
|
||||
(preg_match('/\b(' . self::ADULT_KEYWORDS . ')\b/i', $name) ||
|
||||
preg_match('/\b(' . self::KNOWN_STUDIOS . ')\b/i', $name) ||
|
||||
preg_match('/\b(XXX|Porn|Adult|JAV|Hentai)\b/i', $name))) {
|
||||
return $this->matched(Category::XXX_WEBDL, 0.85, 'webdl');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkX264(string $name): ?CategorizationResult
|
||||
{
|
||||
// Exclude HEVC/x265
|
||||
if (preg_match('/\b(x265|hevc)\b/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Require H.264/x264/AVC
|
||||
if (!preg_match('/\b((x|h)[\.\-_ ]?264|AVC)\b/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reject obvious non-targets
|
||||
if (preg_match('/\bwmv\b|S\d{1,2}E\d{1,2}|\d+x\d+/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for adult content
|
||||
$adultPattern = '/\bXXX\b|a\.b\.erotica|BangBros|Cum|Defloration|Err?oticax?|JoyMii|MetArt|Nubiles|Porn|SexArt|Tushy|Vixen|JAV|Brazzers|NaughtyAmerica|RealityKings/i';
|
||||
|
||||
if (preg_match($adultPattern, $name)) {
|
||||
return $this->matched(Category::XXX_X264, 0.85, 'x264');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkXvid(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/(b[dr]|dvd)rip|detoxication|divx|nympho|pornolation|swe6|tesoro|xvid/i', $name)) {
|
||||
return $this->matched(Category::XXX_XVID, 0.8, 'xvid');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkImageset(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/IMAGESET|PICTURESET|ABPEA/i', $name)) {
|
||||
return $this->matched(Category::XXX_IMAGESET, 0.9, 'imageset');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkWMV(string $name): ?CategorizationResult
|
||||
{
|
||||
// Exclude modern formats
|
||||
if (preg_match('/\b(720p|1080p|2160p|x264|x265|h264|h265|hevc|XviD|MP4-|\.mp4)[._ -]/i', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/\b(WMV|Windows\s?Media\s?Video)\b|\.wmv$|[._ -]wmv[._ -]/i', $name)) {
|
||||
return $this->matched(Category::XXX_WMV, 0.8, 'wmv');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkDVD(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/dvdr[^i]|dvd[59]/i', $name)) {
|
||||
return $this->matched(Category::XXX_DVD, 0.85, 'dvd');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkOther(string $name): ?CategorizationResult
|
||||
{
|
||||
if (preg_match('/[._ -]Brazzers|Creampie|[._ -]JAV[._ -]|North\.Pole|^Nubiles|She[._ -]?Male|Transsexual|OLDER ANGELS/i', $name)) {
|
||||
return $this->matched(Category::XXX_OTHER, 0.7, 'other');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace App\Services\Categorization\Contracts;
|
||||
use App\Services\Categorization\CategorizationResult;
|
||||
use App\Services\Categorization\ReleaseContext;
|
||||
/**
|
||||
* Interface for all categorization handlers.
|
||||
*
|
||||
* Each categorizer is responsible for determining if a release
|
||||
* belongs to its category domain.
|
||||
*/
|
||||
interface CategorizerInterface
|
||||
{
|
||||
/**
|
||||
* Get the priority of this categorizer (lower = higher priority).
|
||||
* Categorizers are executed in priority order.
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
/**
|
||||
* Get the name of this categorizer for debugging/logging.
|
||||
*/
|
||||
public function getName(): string;
|
||||
/**
|
||||
* Attempt to categorize the given release.
|
||||
*
|
||||
* @param ReleaseContext $context The release information
|
||||
* @return CategorizationResult The categorization result
|
||||
*/
|
||||
public function categorize(ReleaseContext $context): CategorizationResult;
|
||||
/**
|
||||
* Check if this categorizer should be skipped for the given context.
|
||||
* Useful for early-exit optimizations.
|
||||
*/
|
||||
public function shouldSkip(ReleaseContext $context): bool;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Categorization;
|
||||
|
||||
/**
|
||||
* Value object containing release information for categorization.
|
||||
*/
|
||||
class ReleaseContext
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $releaseName,
|
||||
public readonly int|string $groupId,
|
||||
public readonly string $groupName = '',
|
||||
public readonly string $poster = '',
|
||||
public readonly bool $categorizeForeign = true,
|
||||
public readonly bool $catWebDL = true,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the release name in lowercase for case-insensitive matching.
|
||||
*/
|
||||
public function getLowerReleaseName(): string
|
||||
{
|
||||
return strtolower($this->releaseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the release name matches a pattern.
|
||||
*/
|
||||
public function matchesPattern(string $pattern): bool
|
||||
{
|
||||
return (bool) preg_match($pattern, $this->releaseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the release name contains a substring (case-insensitive).
|
||||
*/
|
||||
public function containsString(string $needle): bool
|
||||
{
|
||||
return stripos($this->releaseName, $needle) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the group name matches a pattern.
|
||||
*/
|
||||
public function groupMatchesPattern(string $pattern): bool
|
||||
{
|
||||
return (bool) preg_match($pattern, $this->groupName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this release has adult/XXX markers.
|
||||
*/
|
||||
public function hasAdultMarkers(): bool
|
||||
{
|
||||
// Check for explicit XXX markers and common adult keywords
|
||||
if (preg_match('/\b(XXX|Porn|Anal|Brazzers|BangBros|Bangbros|NaughtyAmerica|RealityKings|Tushy|Vixen|Blacked|OnlyFans|MetArt|JoyMii|Creampie|MP4-XXX|PureTaboo|LadyLyne|TeamSkeet|GirlsWay|EvilAngel|Kink|FakeHub|FakeTaxi|SexArt|Nubiles|Defloration|Deeper|Bellesa|Twistys|Mofos|MissaX|LegalPorno|AnalVids|JAV|Hentai)\b/i', $this->releaseName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for adult keywords combined with resolution (likely adult clip)
|
||||
if (preg_match('/\b(Fuck|Fucked|Fucking|Cock|Dick|Pussy|Cum|Cumshot|Blowjob|Handjob|MILF|Teen|Lesbian|Threesome|Gangbang|Hardcore|Interracial)\b/i', $this->releaseName) &&
|
||||
preg_match('/\b(720p|1080p|2160p|4k|mp4)\b/i', $this->releaseName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Services;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use Blacklight\Categorize;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use Blacklight\ElasticSearchSiteSearch;
|
||||
use Blacklight\ManticoreSearch;
|
||||
use Blacklight\ReleaseExtra;
|
||||
@@ -30,7 +30,7 @@ class MediaProcessingService
|
||||
private readonly ReleaseExtra $releaseExtra,
|
||||
private readonly ManticoreSearch $manticore,
|
||||
private readonly ElasticSearchSiteSearch $elasticsearch,
|
||||
private readonly Categorize $categorize,
|
||||
private readonly CategorizationService $categorize,
|
||||
) {}
|
||||
|
||||
public function getVideoTime(string $videoLocation): string
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\Models\Release;
|
||||
use App\Models\ReleaseRegex;
|
||||
use App\Models\ReleasesGroups;
|
||||
use App\Models\UsenetGroup;
|
||||
use Blacklight\Categorize;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use Blacklight\ColorCLI;
|
||||
use Blacklight\NZB;
|
||||
use Blacklight\processing\ProcessReleases;
|
||||
@@ -32,7 +32,7 @@ class ReleaseCreationService
|
||||
public function createReleases(int|string|null $groupID, int $limit, bool $echoCLI): array
|
||||
{
|
||||
$startTime = now()->toImmutable();
|
||||
$categorize = new Categorize;
|
||||
$categorize = new CategorizationService();
|
||||
$returnCount = 0;
|
||||
$duplicate = 0;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\CategorizationServiceProvider::class,
|
||||
App\Providers\ForumServiceProvider::class,
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
|
||||
@@ -4,7 +4,7 @@ require_once dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'bootstrap/autoload.php';
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use Blacklight\Categorize;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use Blacklight\ColorCLI;
|
||||
use Blacklight\ConsoleTools;
|
||||
|
||||
@@ -69,7 +69,7 @@ function categorizeRelease($argv, $echoOutput = false): int
|
||||
$relCount = $chgCount = 0;
|
||||
if ($total > 0) {
|
||||
$query->chunk('100', function ($results) use ($update, $relCount, $chgCount) {
|
||||
$cat = new Categorize;
|
||||
$cat = new CategorizationService();
|
||||
foreach ($results as $result) {
|
||||
$catId = $cat->determineCategory($result->groups_id, $result->searchname, $result->fromname);
|
||||
if ((int) $result->categories_id !== (int) $catId['categories_id']) {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
use Blacklight\Categorize;
|
||||
|
||||
$c = (new ReflectionClass(Categorize::class))->newInstanceWithoutConstructor();
|
||||
$c->poster = '';
|
||||
|
||||
$samples = [
|
||||
'Starfield-RUNE',
|
||||
'Baldurs.Gate.3.TENOKE',
|
||||
'ELDEN.RING-EMPRESS',
|
||||
'Horizon.Zero.Dawn-CODEX',
|
||||
'Cyberpunk.2077.GOG',
|
||||
'Forza.Horizon.5.ElAmigos',
|
||||
'The.Witcher.3.Wild.Hunt.PLaza',
|
||||
'Resident.Evil.4.Remake-FITGIRL',
|
||||
'Red.Dead.Redemption.2.DODI-Repack',
|
||||
'Some.Game.SKiDROW',
|
||||
'Awesome.Game.SteamRip',
|
||||
'Great.Game.Repack-FitGirl',
|
||||
'Indie.Title.DRM-Free.GOG',
|
||||
'Cool.Game.PC.Game.2024',
|
||||
'Windows.10.Title.Repack',
|
||||
'Title-[PC]-DRMFree',
|
||||
];
|
||||
|
||||
foreach ($samples as $name) {
|
||||
$c->releaseName = $name;
|
||||
$res = $c->isPCGame();
|
||||
echo ($res ? 'MATCH' : 'NO-MATCH')."\t$name\n";
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
$rc = new ReflectionClass(\Blacklight\Games::class);
|
||||
$games = $rc->newInstanceWithoutConstructor();
|
||||
|
||||
$inputs = [
|
||||
'[FitGirl] Red.Dead.Redemption.2.v1.0.1436.28.Repack',
|
||||
'Baldurs_Gate_3_v1.0.2_MULTI12-EMPRESS',
|
||||
'Starfield.Update.1.7.29.Patch-FLT',
|
||||
];
|
||||
foreach ($inputs as $in) {
|
||||
$res = $games->parseTitle($in);
|
||||
echo $in, ' => ', json_encode($res, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), "\n";
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Release as ReleaseModel;
|
||||
use App\Services\Categorization\CategorizationService;
|
||||
use App\Services\MediaProcessingService;
|
||||
use Blacklight\Categorize;
|
||||
use Blacklight\ElasticSearchSiteSearch;
|
||||
use Blacklight\ManticoreSearch;
|
||||
use Blacklight\ReleaseExtra;
|
||||
@@ -53,7 +53,7 @@ class MediaProcessingServiceTest extends TestCase
|
||||
?ReleaseExtra $releaseExtra = null,
|
||||
?ManticoreSearch $manticore = null,
|
||||
?ElasticSearchSiteSearch $elastic = null,
|
||||
?Categorize $categorize = null
|
||||
?CategorizationService $categorize = null
|
||||
): MediaProcessingService {
|
||||
$ffmpeg ??= Mockery::mock(FFMpeg::class);
|
||||
$ffprobe ??= Mockery::mock(FFProbe::class);
|
||||
@@ -62,7 +62,7 @@ class MediaProcessingServiceTest extends TestCase
|
||||
$releaseExtra ??= Mockery::mock(ReleaseExtra::class);
|
||||
$manticore ??= Mockery::mock(ManticoreSearch::class);
|
||||
$elastic ??= Mockery::mock(ElasticSearchSiteSearch::class);
|
||||
$categorize ??= Mockery::mock(Categorize::class);
|
||||
$categorize ??= Mockery::mock(CategorizationService::class);
|
||||
|
||||
return new MediaProcessingService($ffmpeg, $ffprobe, $mediaInfo, $releaseImage, $releaseExtra, $manticore, $elastic, $categorize);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user