category = $category ?? new CategorizationService(); $this->fileNameCleaner = $fileNameCleaner ?? new FileNameCleaner(); $this->echoOutput = config('nntmux.echocli'); } /** * Update the release with the new information. * * @param object|array $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) { $newName = (new ReleaseCleaningService())->fixerCleaner($name); // Normalize and sanity-check candidate for non-trusted sources $newName = $this->fileNameCleaner->normalizeCandidateTitle($newName); // Determine if the source is trusted enough to bypass plausibility checks $trustedSource = $this->isTrustedSource($type, $method, $preId); if (!$trustedSource && !$this->fileNameCleaner->isPlausibleReleaseTitle($newName)) { // 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; $determinedCategory = $this->category->determineCategory( $release->groups_id, $newName, !empty($release->fromname) ? $release->fromname : '' ); 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]); if ($this->echoOutput && $show) { $this->echoReleaseInfo($release, $newName, $determinedCategory, $type, $method); } $newTitle = substr($newName, 0, 299); if ($echo === true) { $this->performDatabaseUpdate($release, $newTitle, $determinedCategory, $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. */ 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, array $determinedCategory, string $type, bool $nameStatus, int $preId ): void { if ($nameStatus === true) { $status = $this->getStatusColumnsForType($type); $updateColumns = [ 'videos_id' => 0, 'tv_episodes_id' => 0, 'imdbid' => '', 'musicinfo_id' => '', 'consoleinfo_id' => '', 'bookinfo_id' => '', 'anidbid' => '', 'predb_id' => $preId, 'searchname' => $newTitle, 'categories_id' => $determinedCategory['categories_id'], ]; 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, 'categories_id' => $determinedCategory['categories_id'], 'iscategorized' => 1, ]); } // Update search index Search::updateRelease($release->releases_id); } /** * Get the status columns to update for a given type. */ 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], 'SHA1, ', 'MD5, ' => ['isrenamed' => 1, 'iscategorized' => 1, 'dehashstatus' => 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 ($column !== '' && $id !== 0) { Release::query()->where('id', $id)->update([$column => $status]); } } /** * Check if a release matches a PreDB entry. */ public function checkPreDbMatch(object $release, string $textstring): ?array { if (preg_match_all(self::PREDB_REGEX, $textstring, $hits) && !preg_match('/Source\s\:/i', $textstring)) { foreach ($hits as $hit) { foreach ($hit as $val) { $title = Predb::query()->where('title', trim($val))->select(['title', 'id'])->first(); if ($title !== null) { return ['title' => $title['title'], 'id' => $title['id']]; } } } } 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. */ public function getStats(): array { return [ 'fixed' => $this->fixed, 'checked' => $this->checked, 'matched' => $this->matched, 'done' => $this->done, ]; } }