mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
465 lines
15 KiB
PHP
465 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\NameFixing;
|
|
|
|
use App\Events\ReleaseNameFixed;
|
|
use App\Models\Category;
|
|
use App\Models\Release;
|
|
use App\Models\UsenetGroup;
|
|
use App\Services\AdditionalProcessing\ReleaseSearchSyncCoordinator;
|
|
use App\Services\AdditionalProcessing\State\PersistenceMetricsCollector;
|
|
use App\Services\Categorization\CategorizationService;
|
|
use App\Services\ReleaseCleaningService;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\DB;
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* Service for updating releases with new names.
|
|
*
|
|
* Handles the actual database updates and category re-determination
|
|
* when a release is renamed.
|
|
*/
|
|
class ReleaseUpdateService
|
|
{
|
|
/**
|
|
* @var list<string>
|
|
*/
|
|
private const SINGLE_UPDATE_COLUMNS = [
|
|
'predb_id',
|
|
'proc_nfo',
|
|
'proc_files',
|
|
'proc_par2',
|
|
'proc_uid',
|
|
'proc_hash16k',
|
|
'proc_srr',
|
|
'proc_crc32',
|
|
];
|
|
|
|
/**
|
|
* PreDB regex pattern for scene release names.
|
|
*/
|
|
public const PREDB_REGEX = '/([\w.\'()\[\]-]+(?:[\s._-]+[\w.\'()\[\]-]+)+[-.][\w]+)/ui';
|
|
|
|
// Constants for name fixing status
|
|
public const PROC_NFO_NONE = 0;
|
|
|
|
public const PROC_NFO_DONE = 1;
|
|
|
|
public const PROC_FILES_NONE = 0;
|
|
|
|
public const PROC_FILES_DONE = 1;
|
|
|
|
public const PROC_PAR2_NONE = 0;
|
|
|
|
public const PROC_PAR2_DONE = 1;
|
|
|
|
public const PROC_UID_NONE = 0;
|
|
|
|
public const PROC_UID_DONE = 1;
|
|
|
|
public const PROC_HASH16K_NONE = 0;
|
|
|
|
public const PROC_HASH16K_DONE = 1;
|
|
|
|
public const PROC_SRR_NONE = 0;
|
|
|
|
public const PROC_SRR_DONE = 1;
|
|
|
|
public const PROC_CRC_NONE = 0;
|
|
|
|
public const PROC_CRC_DONE = 1;
|
|
|
|
// Constants for overall rename status
|
|
public const IS_RENAMED_NONE = 0;
|
|
|
|
public const IS_RENAMED_DONE = 1;
|
|
|
|
protected CategorizationService $category;
|
|
|
|
protected FileNameCleaner $fileNameCleaner;
|
|
|
|
protected bool $echoOutput;
|
|
|
|
private readonly ReleaseSearchSyncCoordinator $searchSyncCoordinator;
|
|
|
|
/**
|
|
* The release ID we are trying to rename.
|
|
*/
|
|
protected int $relid = 0;
|
|
|
|
/**
|
|
* Has the current release found a new name?
|
|
*/
|
|
public bool $matched = false;
|
|
|
|
/**
|
|
* Was the check completed?
|
|
*/
|
|
public bool $done = false;
|
|
|
|
/**
|
|
* How many releases have got a new name?
|
|
*/
|
|
public int $fixed = 0;
|
|
|
|
/**
|
|
* How many releases were checked.
|
|
*/
|
|
public int $checked = 0;
|
|
|
|
public function __construct(
|
|
?CategorizationService $category = null,
|
|
?FileNameCleaner $fileNameCleaner = null,
|
|
?ReleaseSearchSyncCoordinator $searchSyncCoordinator = null,
|
|
) {
|
|
$this->category = $category ?? new CategorizationService;
|
|
$this->fileNameCleaner = $fileNameCleaner ?? new FileNameCleaner;
|
|
$this->searchSyncCoordinator = $searchSyncCoordinator
|
|
?? new ReleaseSearchSyncCoordinator(new PersistenceMetricsCollector);
|
|
$this->echoOutput = config('nntmux.echocli');
|
|
}
|
|
|
|
/**
|
|
* Update the release with the new information.
|
|
*
|
|
* @param object|array<string, mixed> $release The release to update
|
|
* @param string $name The new name
|
|
* @param string $method The method that found the name
|
|
* @param bool $echo Whether to actually update the database
|
|
* @param string $type The type string for logging
|
|
* @param bool $nameStatus Whether to update status columns
|
|
* @param bool $show Whether to show output
|
|
* @param int|null $preId PreDB ID if matched
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function updateRelease(
|
|
object|array $release,
|
|
string $name,
|
|
string $method,
|
|
bool $echo,
|
|
string $type,
|
|
bool $nameStatus,
|
|
bool $show,
|
|
?int $preId = 0
|
|
): void {
|
|
$preId = $preId ?? 0;
|
|
if (is_array($release)) {
|
|
$release = (object) $release;
|
|
}
|
|
|
|
// If $release does not have a releases_id, we should add it.
|
|
if (! isset($release->releases_id)) {
|
|
$release->releases_id = $release->id;
|
|
}
|
|
|
|
if ($this->relid !== $release->releases_id) {
|
|
$cleanedName = (new ReleaseCleaningService)->fixerCleaner($name);
|
|
// Normalize and sanity-check candidate for non-trusted sources
|
|
$normalizedName = $this->fileNameCleaner->normalizeCandidateTitle($cleanedName);
|
|
$newName = $this->fileNameCleaner->formatSearchName($cleanedName, $normalizedName);
|
|
|
|
// Determine if the source is trusted enough to bypass plausibility checks
|
|
$trustedSource = $this->isTrustedSource($type, $method, $preId);
|
|
|
|
if (! $trustedSource && ! $this->fileNameCleaner->isPlausibleReleaseTitle($normalizedName)) {
|
|
// Skip low-quality rename candidates for untrusted sources
|
|
$this->done = true;
|
|
|
|
return;
|
|
}
|
|
|
|
if (strtolower($newName) !== strtolower($release->searchname)) {
|
|
$this->matched = true;
|
|
$this->relid = (int) $release->releases_id;
|
|
|
|
if ($type === 'PAR2, ') {
|
|
$newName = ucwords($newName);
|
|
if (preg_match('/(.+?)\.[a-z0-9]{2,3}(PAR2)?$/i', $name, $hit)) {
|
|
$newName = $hit[1];
|
|
}
|
|
}
|
|
|
|
$this->fixed++;
|
|
|
|
// Split on path separator backslash to strip any path
|
|
$newName = explode('\\', $newName);
|
|
$newName = preg_replace(['/^[=_.:\s-]+/', '/[=_.:\s-]+$/'], '', $newName[0]);
|
|
|
|
$newTitle = substr($newName, 0, 299);
|
|
|
|
$determinedCategory = null;
|
|
if ($this->echoOutput && $show) {
|
|
$determinedCategory = $this->category->determineCategory(
|
|
$release->groups_id,
|
|
$newTitle,
|
|
! empty($release->fromname) ? $release->fromname : ''
|
|
);
|
|
|
|
$this->echoReleaseInfo($release, $newTitle, $determinedCategory, $type, $method);
|
|
}
|
|
|
|
if ($echo === true) {
|
|
$this->performDatabaseUpdate($release, $newTitle, $type, $nameStatus, $preId);
|
|
}
|
|
}
|
|
}
|
|
$this->done = true;
|
|
}
|
|
|
|
/**
|
|
* Check if the source is trusted enough to bypass plausibility checks.
|
|
*/
|
|
protected function isTrustedSource(string $type, string $method, int $preId): bool
|
|
{
|
|
return
|
|
(! empty($preId) && $preId > 0) ||
|
|
str_starts_with($type, 'PreDB') ||
|
|
str_starts_with($type, 'PreDb') ||
|
|
$type === 'UID, ' ||
|
|
$type === 'PAR2 hash, ' ||
|
|
$type === 'CRC32, ' ||
|
|
$type === 'SRR, ' ||
|
|
stripos($method, 'Title Match') !== false ||
|
|
stripos($method, 'file matched source') !== false ||
|
|
stripos($method, 'PreDb') !== false ||
|
|
stripos($method, 'preDB') !== false;
|
|
}
|
|
|
|
/**
|
|
* Echo release information to CLI.
|
|
*
|
|
* @param array<string, mixed> $determinedCategory
|
|
*/
|
|
public function echoReleaseInfo(
|
|
object $release,
|
|
string $newName,
|
|
array $determinedCategory,
|
|
string $type,
|
|
string $method
|
|
): void {
|
|
$groupName = UsenetGroup::getNameByID($release->groups_id);
|
|
$oldCatName = Category::getNameByID($release->categories_id);
|
|
$newCatName = Category::getNameByID($determinedCategory['categories_id']);
|
|
|
|
if ($type === 'PAR2, ') {
|
|
echo PHP_EOL;
|
|
}
|
|
|
|
echo PHP_EOL;
|
|
|
|
cli()->primary('Release Information:');
|
|
|
|
echo ' '.cli()->headerOver('New name: ').cli()->primary(substr($newName, 0, 100)).PHP_EOL;
|
|
echo ' '.cli()->headerOver('Old name: ').cli()->primary(substr((string) $release->searchname, 0, 100)).PHP_EOL;
|
|
echo ' '.cli()->headerOver('Use name: ').cli()->primary(substr((string) $release->name, 0, 100)).PHP_EOL;
|
|
echo PHP_EOL;
|
|
|
|
echo ' '.cli()->headerOver('New cat: ').cli()->primary($newCatName).PHP_EOL;
|
|
echo ' '.cli()->headerOver('Old cat: ').cli()->primary($oldCatName).PHP_EOL;
|
|
echo ' '.cli()->headerOver('Group: ').cli()->primary($groupName).PHP_EOL;
|
|
echo PHP_EOL;
|
|
|
|
echo ' '.cli()->headerOver('Method: ').cli()->primary($type.$method).PHP_EOL;
|
|
echo ' '.cli()->headerOver('Release ID: ').cli()->primary((string) $release->releases_id).PHP_EOL;
|
|
|
|
if (! empty($release->filename)) {
|
|
echo ' '.cli()->headerOver('Filename: ').cli()->primary(substr((string) $release->filename, 0, 100)).PHP_EOL;
|
|
}
|
|
|
|
if ($type !== 'PAR2, ') {
|
|
echo PHP_EOL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform the actual database update.
|
|
*/
|
|
protected function performDatabaseUpdate(
|
|
object $release,
|
|
string $newTitle,
|
|
string $type,
|
|
bool $nameStatus,
|
|
int $preId
|
|
): void {
|
|
DB::transaction(function () use ($release, $newTitle, $type, $nameStatus, $preId): void {
|
|
if ($nameStatus === true) {
|
|
$status = $this->getStatusColumnsForType($type);
|
|
|
|
$updateColumns = [
|
|
'videos_id' => 0,
|
|
'tv_episodes_id' => 0,
|
|
'imdbid' => null,
|
|
'musicinfo_id' => '',
|
|
'consoleinfo_id' => '',
|
|
'bookinfo_id' => '',
|
|
'anidbid' => '',
|
|
'predb_id' => $preId,
|
|
'searchname' => $newTitle,
|
|
];
|
|
|
|
if (! empty($status)) {
|
|
foreach ($status as $key => $stat) {
|
|
$updateColumns = Arr::add($updateColumns, $key, $stat);
|
|
}
|
|
}
|
|
|
|
Release::query()
|
|
->where('id', $release->releases_id)
|
|
->update($updateColumns);
|
|
} else {
|
|
Release::query()
|
|
->where('id', $release->releases_id)
|
|
->update([
|
|
'videos_id' => 0,
|
|
'tv_episodes_id' => 0,
|
|
'imdbid' => null,
|
|
'musicinfo_id' => null,
|
|
'consoleinfo_id' => null,
|
|
'bookinfo_id' => null,
|
|
'anidbid' => null,
|
|
'predb_id' => $preId,
|
|
'searchname' => $newTitle,
|
|
'iscategorized' => 1,
|
|
]);
|
|
}
|
|
|
|
event(new ReleaseNameFixed(
|
|
(int) $release->releases_id,
|
|
(string) $release->searchname,
|
|
$newTitle,
|
|
(int) $release->categories_id,
|
|
$release->groups_id,
|
|
(string) ($release->fromname ?? '')
|
|
));
|
|
});
|
|
|
|
$this->searchSyncCoordinator->request((int) $release->releases_id);
|
|
}
|
|
|
|
/**
|
|
* Get the status columns to update for a given type.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function getStatusColumnsForType(string $type): array
|
|
{
|
|
return match ($type) {
|
|
'NFO, ' => ['isrenamed' => 1, 'iscategorized' => 1, 'proc_nfo' => 1],
|
|
'PAR2, ' => ['isrenamed' => 1, 'iscategorized' => 1, 'proc_par2' => 1],
|
|
'Filenames, ', 'file matched source: ' => ['isrenamed' => 1, 'iscategorized' => 1, 'proc_files' => 1],
|
|
'PreDB FT Exact, ' => ['isrenamed' => 1, 'iscategorized' => 1],
|
|
'sorter, ' => ['isrenamed' => 1, 'iscategorized' => 1, 'proc_sorter' => 1],
|
|
'UID, ', 'Mediainfo, ' => ['isrenamed' => 1, 'iscategorized' => 1, 'proc_uid' => 1],
|
|
'PAR2 hash, ' => ['isrenamed' => 1, 'iscategorized' => 1, 'proc_hash16k' => 1],
|
|
'SRR, ' => ['isrenamed' => 1, 'iscategorized' => 1, 'proc_srr' => 1],
|
|
'CRC32, ' => ['isrenamed' => 1, 'iscategorized' => 1, 'proc_crc32' => 1],
|
|
default => [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update a single column in releases.
|
|
*/
|
|
public function updateSingleColumn(string $column, int $status, int $id): void
|
|
{
|
|
if (! in_array($column, self::SINGLE_UPDATE_COLUMNS, true)) {
|
|
throw new InvalidArgumentException("Unsupported release status column [{$column}].");
|
|
}
|
|
|
|
if ($id !== 0) {
|
|
DB::update("UPDATE releases SET {$column} = ? WHERE id = ?", [$status, $id]);
|
|
}
|
|
}
|
|
|
|
public function attachPredbId(int $releaseId, int $predbId): void
|
|
{
|
|
if ($releaseId === 0 || $predbId === 0) {
|
|
return;
|
|
}
|
|
|
|
$this->updateSingleColumn('predb_id', $predbId, $releaseId);
|
|
$this->relid = $releaseId;
|
|
$this->matched = true;
|
|
$this->done = true;
|
|
}
|
|
|
|
/**
|
|
* Check if a release matches a PreDB entry.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function checkPreDbMatch(object $release, string $textstring): ?array
|
|
{
|
|
if (! preg_match_all(self::PREDB_REGEX, $textstring, $hits) || preg_match('/Source\s:/i', $textstring)) {
|
|
return null;
|
|
}
|
|
|
|
$candidates = [];
|
|
foreach ($hits as $hit) {
|
|
foreach ($hit as $value) {
|
|
$title = trim($value);
|
|
if ($title !== '') {
|
|
$candidates[$title] = $title;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($candidates === []) {
|
|
return null;
|
|
}
|
|
|
|
$candidateValues = array_values($candidates);
|
|
$rows = DB::select(
|
|
'SELECT id, title FROM predb WHERE title IN ('.implode(',', array_fill(0, count($candidateValues), '?')).')',
|
|
$candidateValues
|
|
);
|
|
$matches = [];
|
|
foreach ($rows as $row) {
|
|
$matches[(string) $row->title] = ['title' => (string) $row->title, 'id' => (int) $row->id];
|
|
}
|
|
|
|
foreach ($candidateValues as $candidate) {
|
|
if (isset($matches[$candidate])) {
|
|
return $matches[$candidate];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Reset status variables for new processing.
|
|
*/
|
|
public function reset(): void
|
|
{
|
|
$this->done = $this->matched = false;
|
|
}
|
|
|
|
/**
|
|
* Increment the checked counter.
|
|
*/
|
|
public function incrementChecked(): void
|
|
{
|
|
$this->checked++;
|
|
}
|
|
|
|
/**
|
|
* Get the current statistics.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getStats(): array
|
|
{
|
|
return [
|
|
'fixed' => $this->fixed,
|
|
'checked' => $this->checked,
|
|
'matched' => $this->matched,
|
|
'done' => $this->done,
|
|
];
|
|
}
|
|
}
|