mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update book processing further
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Facades\Search;
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use App\Services\NameFixing\Extractors\ObfuscatedSubjectExtractor;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RequeueFailedBooks extends Command
|
||||
{
|
||||
protected $signature = 'books:requeue-failed
|
||||
{--dry-run : Preview affected releases without writing changes}
|
||||
{--limit=0 : Max releases to inspect (0 = no limit)}
|
||||
{--only-obfuscated : Only requeue rows with obfuscated subject that can be normalized}
|
||||
{--normalize-obfuscated-all : Normalize obfuscated names across all book/audiobook releases, not just bookinfo_id=-2}';
|
||||
|
||||
protected $description = 'Requeue failed book metadata rows (bookinfo_id = -2) and normalize obfuscated search names.';
|
||||
|
||||
public function handle(ObfuscatedSubjectExtractor $extractor): int
|
||||
{
|
||||
$limit = max(0, (int) $this->option('limit'));
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$onlyObfuscated = (bool) $this->option('only-obfuscated');
|
||||
$normalizeObfuscatedAll = (bool) $this->option('normalize-obfuscated-all');
|
||||
|
||||
$query = Release::query()
|
||||
->select(['id', 'name', 'searchname', 'categories_id', 'bookinfo_id'])
|
||||
->where(function ($builder): void {
|
||||
$builder->whereBetween('categories_id', [Category::BOOKS_ROOT, Category::BOOKS_UNKNOWN])
|
||||
->orWhere('categories_id', Category::MUSIC_AUDIOBOOK);
|
||||
})
|
||||
->orderBy('id');
|
||||
|
||||
if ($normalizeObfuscatedAll) {
|
||||
$query->where(function ($builder): void {
|
||||
$builder
|
||||
->where('searchname', 'like', 'N:/NZB%')
|
||||
->orWhere('searchname', 'like', 'N_NZB_%')
|
||||
->orWhere('name', 'like', 'N:/NZB%')
|
||||
->orWhere('name', 'like', 'N_NZB_%');
|
||||
});
|
||||
} else {
|
||||
$query->where('bookinfo_id', -2);
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
$releases = $query->get();
|
||||
$total = $releases->count();
|
||||
|
||||
if ($total === 0) {
|
||||
$this->info('No failed book releases found (bookinfo_id = -2).');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$queued = 0;
|
||||
$renamed = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$bar->start();
|
||||
|
||||
foreach ($releases as $release) {
|
||||
$bar->advance();
|
||||
|
||||
$normalized = $extractor->extract((string) $release->searchname)
|
||||
?? $extractor->extract((string) $release->name);
|
||||
|
||||
if ($onlyObfuscated && $normalized === null) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$updates = [];
|
||||
$shouldRequeue = (int) $release->bookinfo_id === -2;
|
||||
if ($shouldRequeue) {
|
||||
$updates['bookinfo_id'] = null;
|
||||
}
|
||||
if ($normalized !== null && $normalized !== $release->searchname) {
|
||||
$updates['searchname'] = $normalized;
|
||||
$updates['isrenamed'] = 1;
|
||||
}
|
||||
|
||||
if ($updates === []) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
if (isset($updates['searchname'])) {
|
||||
$this->newLine();
|
||||
$this->line(sprintf(
|
||||
'[dry-run] #%d searchname: "%s" -> "%s"',
|
||||
(int) $release->id,
|
||||
(string) $release->searchname,
|
||||
(string) $updates['searchname']
|
||||
));
|
||||
}
|
||||
|
||||
$queued++;
|
||||
if (isset($updates['searchname'])) {
|
||||
$renamed++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Release::query()->where('id', $release->id)->update($updates);
|
||||
Search::updateRelease((int) $release->id);
|
||||
|
||||
$queued++;
|
||||
if (isset($updates['searchname'])) {
|
||||
$renamed++;
|
||||
}
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info(sprintf(
|
||||
'Dry run complete. Inspected: %d, would apply updates: %d, would normalize names: %d, skipped: %d.',
|
||||
$total,
|
||||
$queued,
|
||||
$renamed,
|
||||
$skipped
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(sprintf(
|
||||
'Requeued %d failed book release(s). Normalized %d searchname value(s). Skipped: %d.',
|
||||
$queued,
|
||||
$renamed,
|
||||
$skipped
|
||||
));
|
||||
$this->line('Next step: run your books postprocess (e.g. `php artisan update:postprocess book`).');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -169,10 +169,14 @@ class ReleaseFileManager
|
||||
*/
|
||||
public function processReleaseNameFromNzbContents(array $nzbContents, ReleaseProcessingContext $context): bool
|
||||
{
|
||||
if (! $this->releaseHasNzbSplitWrapper($context->release)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($nzbContents as $nzbFile) {
|
||||
$title = (string) ($nzbFile['title'] ?? '');
|
||||
|
||||
if ($title === '' || ! str_contains($title, '__NZBSPLIT__')) {
|
||||
if ($title === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -184,7 +188,8 @@ class ReleaseFileManager
|
||||
|
||||
$candidate = $this->fileNameCleaner->normalizeCandidateTitle($candidate);
|
||||
|
||||
if (! $this->fileNameCleaner->isPlausibleReleaseTitle($candidate)) {
|
||||
$isPlausible = $this->fileNameCleaner->isPlausibleReleaseTitle($candidate);
|
||||
if (! $isPlausible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -651,6 +656,15 @@ class ReleaseFileManager
|
||||
return $this->fileNameCleaner->normalizeCandidateTitle($title);
|
||||
}
|
||||
|
||||
private function releaseHasNzbSplitWrapper(Release $release): bool
|
||||
{
|
||||
$name = (string) ($release->name ?? '');
|
||||
$searchName = (string) ($release->searchname ?? '');
|
||||
|
||||
return str_contains(strtoupper($name), 'NZBSPLIT')
|
||||
|| str_contains(strtoupper($searchName), 'NZBSPLIT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a title is plausible for release naming.
|
||||
*/
|
||||
|
||||
@@ -191,13 +191,13 @@ class BookService
|
||||
}
|
||||
|
||||
// Step 2: Get paginated book entity list with only needed columns
|
||||
$bookSql = 'SELECT boo.id, boo.title, boo.author, boo.cover, boo.publisher, boo.publishdate, boo.review, boo.url, '
|
||||
$bookSql = 'SELECT boo.id, boo.title, boo.author, boo.cover, boo.publisher, boo.publishdate, boo.overview, boo.url, '
|
||||
.'MAX(r.postdate) AS latest_postdate, '
|
||||
.'COUNT(r.id) AS total_releases '
|
||||
.'FROM bookinfo boo '
|
||||
.'INNER JOIN releases r ON boo.id = r.bookinfo_id '
|
||||
.'WHERE '.$baseWhere.' '
|
||||
.'GROUP BY boo.id, boo.title, boo.author, boo.cover, boo.publisher, boo.publishdate, boo.review, boo.url '
|
||||
.'GROUP BY boo.id, boo.title, boo.author, boo.cover, boo.publisher, boo.publishdate, boo.overview, boo.url '
|
||||
."ORDER BY {$order[0]} {$order[1]} "
|
||||
."LIMIT {$num} OFFSET {$start}";
|
||||
|
||||
@@ -366,6 +366,8 @@ class BookService
|
||||
*/
|
||||
public function processBookReleases(string $groupID = '', string $guidChar = ''): void
|
||||
{
|
||||
$this->normalizeBookSearchNames($groupID, $guidChar);
|
||||
|
||||
$query = Release::query()
|
||||
->whereNull('bookinfo_id')
|
||||
->where(static function ($builder): void {
|
||||
@@ -394,6 +396,50 @@ class BookService
|
||||
);
|
||||
}
|
||||
|
||||
protected function normalizeBookSearchNames(string $groupID = '', string $guidChar = ''): void
|
||||
{
|
||||
$query = Release::query()
|
||||
->select(['id', 'name', 'searchname', 'categories_id', 'isrenamed'])
|
||||
->where(static function ($builder): void {
|
||||
$builder->whereBetween('categories_id', [Category::BOOKS_ROOT, Category::BOOKS_UNKNOWN])
|
||||
->orWhere('categories_id', Category::MUSIC_AUDIOBOOK);
|
||||
})
|
||||
->where(function ($builder): void {
|
||||
$builder->where('isrenamed', 0)
|
||||
->orWhere('searchname', 'like', 'N:/NZB%')
|
||||
->orWhere('searchname', 'like', 'N_NZB_%')
|
||||
->orWhere('name', 'like', 'N:/NZB%')
|
||||
->orWhere('name', 'like', 'N_NZB_%');
|
||||
})
|
||||
->orderByDesc('postdate')
|
||||
->limit($this->bookqty);
|
||||
|
||||
if ($guidChar !== '') {
|
||||
$query->where('leftguid', 'like', $guidChar.'%');
|
||||
}
|
||||
|
||||
if ($groupID !== '') {
|
||||
$query->where('groups_id', $groupID);
|
||||
}
|
||||
|
||||
foreach ($query->get() as $release) {
|
||||
$releaseType = (int) $release->categories_id === Category::MUSIC_AUDIOBOOK ? 'audiobook' : 'ebook';
|
||||
$sourceName = $this->preferredBookSourceName((string) $release->searchname, (string) $release->name);
|
||||
$parsed = $this->parseReleaseName($sourceName, $releaseType);
|
||||
$normalizedSearchName = $this->determineReadableBookSearchName($sourceName, $parsed);
|
||||
|
||||
if ($normalizedSearchName === null || $normalizedSearchName === $release->searchname) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Release::query()->where('id', (int) $release->id)->update([
|
||||
'searchname' => $normalizedSearchName,
|
||||
'isrenamed' => 1,
|
||||
]);
|
||||
Search::updateRelease((int) $release->id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process book releases helper.
|
||||
*
|
||||
@@ -472,8 +518,16 @@ class BookService
|
||||
*/
|
||||
public function parseTitle(mixed $release_name, mixed $releaseID, mixed $releasetype)
|
||||
{
|
||||
$normalizedReleaseName = $this->obfuscatedSubjectExtractor->extract((string) $release_name) ?? (string) $release_name;
|
||||
$parsed = $this->parseReleaseName($normalizedReleaseName, (string) $releasetype);
|
||||
$rawReleaseName = (string) $release_name;
|
||||
$parsed = $this->parseReleaseName($rawReleaseName, (string) $releasetype);
|
||||
$normalizedReleaseName = $this->determineReadableBookSearchName($rawReleaseName, $parsed) ?? $rawReleaseName;
|
||||
if ($normalizedReleaseName !== $rawReleaseName) {
|
||||
Release::query()->where('id', (int) $releaseID)->update([
|
||||
'searchname' => $normalizedReleaseName,
|
||||
'isrenamed' => 1,
|
||||
]);
|
||||
Search::updateRelease((int) $releaseID);
|
||||
}
|
||||
$this->parsedBookResult = $parsed;
|
||||
$this->parsedIsbn = $parsed->isbn;
|
||||
$releasename = $parsed->title;
|
||||
@@ -528,6 +582,7 @@ class BookService
|
||||
|
||||
public function parseReleaseName(string $releaseName, string $releaseType = 'ebook'): BookParseResult
|
||||
{
|
||||
$rawReleaseName = $releaseName;
|
||||
$releaseName = $this->obfuscatedSubjectExtractor->extract($releaseName) ?? $releaseName;
|
||||
if (preg_match('/"([^"]{3,240})"/', $releaseName, $quotedMatch) === 1) {
|
||||
$releaseName = $quotedMatch[1];
|
||||
@@ -544,6 +599,13 @@ class BookService
|
||||
$normalized = trim((string) preg_replace('/\s+/', ' ', $normalized));
|
||||
|
||||
$year = null;
|
||||
$specialMagazineTitle = null;
|
||||
if (preg_match('/^MCN[._ -](?<month>[A-Za-z]+)[._ -](?<day>\d{1,2})[._ -](?<year>(?:19|20)\d{2})/i', $rawReleaseName, $mcnMatch) === 1) {
|
||||
$month = ucfirst(strtolower($mcnMatch['month']));
|
||||
$day = ltrim($mcnMatch['day'], '0');
|
||||
$specialMagazineTitle = sprintf('MCN - %s %s, %s', $month, $day === '' ? $mcnMatch['day'] : $day, $mcnMatch['year']);
|
||||
$year = (int) $mcnMatch['year'];
|
||||
}
|
||||
if (preg_match('/\b(19|20)\d{2}\b/', $normalized, $yearMatch) === 1) {
|
||||
$year = (int) $yearMatch[0];
|
||||
}
|
||||
@@ -554,11 +616,13 @@ class BookService
|
||||
}
|
||||
|
||||
$author = null;
|
||||
$title = $normalized;
|
||||
if (preg_match('/^(?<author>[A-Za-z0-9&\'\.\-\s]{3,80})\s-\s(?<title>.+)$/', $normalized, $matches) === 1) {
|
||||
$title = $specialMagazineTitle ?? $normalized;
|
||||
if ($specialMagazineTitle === null && preg_match('/^(?<author>[A-Za-z0-9&\'\.\-\s]{3,80})\s-\s(?<title>.+)$/', $normalized, $matches) === 1) {
|
||||
$candidateAuthor = trim($matches['author']);
|
||||
$candidateTitle = trim($matches['title']);
|
||||
if (preg_match('/^(Issue\s+\d+|\d+\w*\s+Edition\b)/i', $candidateTitle) !== 1) {
|
||||
$isMcnMagazineSplit = preg_match('/^MCN$/i', $candidateAuthor) === 1
|
||||
&& preg_match('/^\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\b/i', $candidateTitle) === 1;
|
||||
if ($isMcnMagazineSplit === false && preg_match('/^(Issue\s+\d+|\d+\w*\s+Edition\b)/i', $candidateTitle) !== 1) {
|
||||
$author = $candidateAuthor;
|
||||
$title = $candidateTitle;
|
||||
}
|
||||
@@ -606,6 +670,11 @@ class BookService
|
||||
preg_match('/^([a-z0-9ü!]+ ){1,2}(N|Vol)?\d{1,4}([abc])?$|^([a-z0-9]+ ){1,2}(Jan( |unar|$)|Feb( |ruary|$)|Mar( |ch|$)|Apr( |il|$)|May(?![a-z0-9])|Jun([ e$])|Jul([ y$])|Aug( |ust|$)|Sep( |tember|$)|O([ck])t( |ober|$)|Nov( |ember|$)|De([cz])( |ember|$))/ui', $normalized) === 1
|
||||
|| preg_match('/\bIssue[\._\- ]?\d{1,4}\b.*\b(19|20)\d{2}\b/i', $normalized) === 1
|
||||
|| preg_match('/\bIssue[\._\- ]?\d{1,4}\b/i', $normalized) === 1
|
||||
|| (preg_match('/\bMCN[._ -]/i', $rawReleaseName) === 1
|
||||
&& preg_match('/\bMAGAZINE\b/i', $rawReleaseName) === 1)
|
||||
|| (preg_match('/^MCN\b/i', $normalized) === 1
|
||||
&& preg_match('/\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\b/i', $normalized) === 1
|
||||
&& preg_match('/\b\d{1,2}\b/', $normalized) === 1)
|
||||
) && preg_match('/Part \d+/i', $normalized) !== 1;
|
||||
|
||||
return new BookParseResult(
|
||||
@@ -639,6 +708,39 @@ class BookService
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function determineReadableBookSearchName(string $rawReleaseName, BookParseResult $parsed): ?string
|
||||
{
|
||||
if ($this->looksLikeObfuscatedBookSubject($rawReleaseName)) {
|
||||
$normalized = $this->obfuscatedSubjectExtractor->extract($rawReleaseName);
|
||||
if ($normalized !== null) {
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parsed->isJunk) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$candidate = trim($parsed->searchQuery());
|
||||
if ($candidate === '' || preg_match('/^[a-z0-9]+$|^([0-9]+ ){1,}$|Part \d+/i', $candidate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$wordCount = count(preg_split('/\s+/', $candidate) ?: []);
|
||||
|
||||
return $wordCount >= 2 ? $candidate : null;
|
||||
}
|
||||
|
||||
protected function preferredBookSourceName(string $searchName, string $originalName): string
|
||||
{
|
||||
return trim($searchName) !== '' ? $searchName : $originalName;
|
||||
}
|
||||
|
||||
protected function looksLikeObfuscatedBookSubject(string $value): bool
|
||||
{
|
||||
return preg_match('/^\s*(?:N:\/NZB\b|N[\s._:-]*NZB[\s._-]*\[|(?:re\s*)?posted\s+by\b)/i', $value) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update book info from external sources.
|
||||
*
|
||||
@@ -883,7 +985,7 @@ class BookService
|
||||
}
|
||||
}
|
||||
|
||||
return $bestScore >= 0.55 ? $best : null;
|
||||
return $bestScore >= $this->minimumMatchScore($parsed) ? $best : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -906,10 +1008,20 @@ class BookService
|
||||
}
|
||||
}
|
||||
|
||||
if ($best === null || $bestScore < 0.55) {
|
||||
if ($best === null || $bestScore < $this->minimumMatchScore($parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $best;
|
||||
}
|
||||
|
||||
private function minimumMatchScore(?BookParseResult $parsed): float
|
||||
{
|
||||
if ($parsed === null) {
|
||||
return 0.55;
|
||||
}
|
||||
|
||||
// No-author parses are more ambiguous and need a stricter cutoff.
|
||||
return $parsed->hasAuthor() ? 0.55 : 0.68;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ final class ObfuscatedSubjectExtractor
|
||||
*/
|
||||
private const array PREFIX_PATTERNS = [
|
||||
'/^\s*N:\/NZB\s*\[\d+\/\d+\]\s*-\s*/i',
|
||||
'/^\s*N[\s._:-]*NZB[\s._-]*\[\d+(?:[\/_]\d+)?\][\s._-]*-\s*/i',
|
||||
'/^\s*\[[^\]]{2,80}\]\s*/',
|
||||
'/^\s*(?:re\s*)?posted\s+by\s+[^-]{2,120}\s*-\s*/i',
|
||||
];
|
||||
@@ -25,6 +26,7 @@ final class ObfuscatedSubjectExtractor
|
||||
'/\.7z$/i',
|
||||
'/\.rar$/i',
|
||||
'/\.par2?$/i',
|
||||
'/\.part$/i',
|
||||
'/\.vol\d+[+\-]\d+\.par2?$/i',
|
||||
'/\.\d{3}$/',
|
||||
];
|
||||
@@ -64,6 +66,7 @@ final class ObfuscatedSubjectExtractor
|
||||
$normalized = preg_replace('/\.(?=[A-Za-z0-9])/', ' ', $normalized) ?? $normalized;
|
||||
$normalized = preg_replace('/\s{2,}/', ' ', $normalized) ?? $normalized;
|
||||
$normalized = trim($normalized, " \t\n\r\0\x0B\"'`-_.");
|
||||
$normalized = $this->toReadableTitle($normalized);
|
||||
|
||||
if ($normalized === '' || $normalized === $original) {
|
||||
return null;
|
||||
@@ -71,4 +74,15 @@ final class ObfuscatedSubjectExtractor
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function toReadableTitle(string $value): string
|
||||
{
|
||||
// If the candidate looks fully lowercase, present a cleaner title-cased
|
||||
// variant for UI/searchname while preserving separators and numbers.
|
||||
if (preg_match('/\p{Lu}/u', $value) === 1) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,44 @@ class FileNameCleaner
|
||||
return trim($t, " .-_\t\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a candidate for storage in searchname.
|
||||
*/
|
||||
public function formatSearchName(string $title, ?string $normalizedFallback = null): string
|
||||
{
|
||||
$formatted = trim($title);
|
||||
|
||||
$formatted = $this->extractNzbSplitName($formatted) ?? $formatted;
|
||||
|
||||
// Remove common media/archive extensions while keeping scene separators.
|
||||
$formatted = preg_replace(self::VIDEO_EXTENSIONS, '', $formatted) ?? $formatted;
|
||||
$formatted = preg_replace(self::AUDIO_EXTENSIONS, '', $formatted) ?? $formatted;
|
||||
$formatted = preg_replace(self::IMAGE_EXTENSIONS, '', $formatted) ?? $formatted;
|
||||
$formatted = preg_replace(self::EBOOK_EXTENSIONS, '', $formatted) ?? $formatted;
|
||||
$formatted = preg_replace(self::GAMEAPP_EXTENSIONS, '', $formatted) ?? $formatted;
|
||||
$formatted = preg_replace(self::SUBTITLE_EXTENSIONS, '', $formatted) ?? $formatted;
|
||||
|
||||
foreach (self::ARCHIVE_PATTERNS as $pattern) {
|
||||
$formatted = preg_replace($pattern, '', $formatted) ?? $formatted;
|
||||
}
|
||||
|
||||
$formatted = preg_replace('/[.\-_ ](?:part|vol|r)\d+(?:\+\d+)?$/i', '', $formatted) ?? $formatted;
|
||||
$formatted = trim($formatted, " .-_\t\r\n");
|
||||
|
||||
if ($formatted === '') {
|
||||
return $normalizedFallback ?? $this->normalizeCandidateTitle($title);
|
||||
}
|
||||
|
||||
if (! $this->looksLikeSceneRelease($formatted)) {
|
||||
return $normalizedFallback ?? $this->normalizeCandidateTitle($title);
|
||||
}
|
||||
|
||||
$formatted = preg_replace('/[ _]+/', '.', $formatted) ?? $formatted;
|
||||
$formatted = preg_replace('/\.{2,}/', '.', $formatted) ?? $formatted;
|
||||
|
||||
return trim($formatted, " .-_\t\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a title is plausible for a release.
|
||||
*
|
||||
|
||||
@@ -137,14 +137,15 @@ class ReleaseUpdateService
|
||||
}
|
||||
|
||||
if ($this->relid !== $release->releases_id) {
|
||||
$newName = (new ReleaseCleaningService)->fixerCleaner($name);
|
||||
$cleanedName = (new ReleaseCleaningService)->fixerCleaner($name);
|
||||
// Normalize and sanity-check candidate for non-trusted sources
|
||||
$newName = $this->fileNameCleaner->normalizeCandidateTitle($newName);
|
||||
$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($newName)) {
|
||||
if (! $trustedSource && ! $this->fileNameCleaner->isPlausibleReleaseTitle($normalizedName)) {
|
||||
// Skip low-quality rename candidates for untrusted sources
|
||||
$this->done = true;
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PostProcessRunner extends BaseRunner
|
||||
{
|
||||
private function guidBucketExpression(string $column = 'leftguid'): string
|
||||
{
|
||||
return DB::getDriverName() === 'sqlite'
|
||||
? 'substr('.$column.', 1, 1)'
|
||||
: 'LEFT('.$column.', 1)';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $releases
|
||||
*/
|
||||
@@ -40,6 +47,17 @@ class PostProcessRunner extends BaseRunner
|
||||
$count = count($releases);
|
||||
$this->headerStart('postprocess: '.$desc, $count, $maxProcesses);
|
||||
|
||||
if ($count <= 1 || $maxProcesses <= 1) {
|
||||
foreach ($releases as $release) {
|
||||
$char = isset($release->id) ? substr((string) $release->id, 0, 1) : '';
|
||||
$command = PHP_BINARY.' artisan postprocess:guid '.$type.' '.$char;
|
||||
echo $this->executeCommand($command);
|
||||
cli()->primary('Finished task for '.$desc);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Process in batches using Laravel's native Concurrency facade
|
||||
$batches = array_chunk($releases, max(1, $maxProcesses));
|
||||
|
||||
@@ -68,13 +86,14 @@ class PostProcessRunner extends BaseRunner
|
||||
|
||||
public function processAdditional(): void
|
||||
{
|
||||
$bucketExpr = $this->guidBucketExpression('r.leftguid');
|
||||
$ppAddMinSize = Settings::settingValue('minsizetopostprocess') !== '' ? (int) Settings::settingValue('minsizetopostprocess') : 1;
|
||||
$ppAddMinSize = ($ppAddMinSize > 0 ? ('AND r.size > '.($ppAddMinSize * 1048576)) : '');
|
||||
$ppAddMaxSize = (Settings::settingValue('maxsizetopostprocess') !== '') ? (int) Settings::settingValue('maxsizetopostprocess') : 100;
|
||||
$ppAddMaxSize = ($ppAddMaxSize > 0 ? ('AND r.size < '.($ppAddMaxSize * 1073741824)) : '');
|
||||
|
||||
$sql = '
|
||||
SELECT DISTINCT LEFT(r.leftguid, 1) AS id
|
||||
SELECT DISTINCT '.$bucketExpr.' AS id
|
||||
FROM releases r
|
||||
LEFT JOIN categories c ON c.id = r.categories_id
|
||||
WHERE r.passwordstatus = -1
|
||||
@@ -105,8 +124,9 @@ class PostProcessRunner extends BaseRunner
|
||||
return;
|
||||
}
|
||||
|
||||
$bucketExpr = $this->guidBucketExpression('r.leftguid');
|
||||
$sql = '
|
||||
SELECT DISTINCT LEFT(r.leftguid, 1) AS id
|
||||
SELECT DISTINCT '.$bucketExpr.' AS id
|
||||
FROM releases r
|
||||
WHERE 1=1 '.$nfoQuery.'
|
||||
LIMIT 16';
|
||||
@@ -141,8 +161,9 @@ class PostProcessRunner extends BaseRunner
|
||||
}
|
||||
|
||||
$renamedFlag = ($renamedOnly ? 2 : 1);
|
||||
$bucketExpr = $this->guidBucketExpression();
|
||||
$sql = '
|
||||
SELECT DISTINCT LEFT(leftguid, 1) AS id, '.$renamedFlag.' AS renamed
|
||||
SELECT DISTINCT '.$bucketExpr.' AS id, '.$renamedFlag.' AS renamed
|
||||
FROM releases
|
||||
WHERE categories_id BETWEEN 2000 AND 2999
|
||||
AND '.imdb_id_needs_lookup_sql('imdbid').'
|
||||
@@ -182,8 +203,9 @@ class PostProcessRunner extends BaseRunner
|
||||
}
|
||||
|
||||
$renamedFlag = ($renamedOnly ? 2 : 1);
|
||||
$bucketExpr = $this->guidBucketExpression();
|
||||
$sql = '
|
||||
SELECT DISTINCT LEFT(leftguid, 1) AS id, '.$renamedFlag.' AS renamed
|
||||
SELECT DISTINCT '.$bucketExpr.' AS id, '.$renamedFlag.' AS renamed
|
||||
FROM releases
|
||||
WHERE categories_id BETWEEN 5000 AND 5999
|
||||
AND categories_id != 5070
|
||||
@@ -304,8 +326,9 @@ class PostProcessRunner extends BaseRunner
|
||||
return;
|
||||
}
|
||||
|
||||
$bucketExpr = $this->guidBucketExpression();
|
||||
$sql = '
|
||||
SELECT DISTINCT LEFT(leftguid, 1) AS id
|
||||
SELECT DISTINCT '.$bucketExpr.' AS id
|
||||
FROM releases
|
||||
WHERE categories_id = 5070
|
||||
AND anidbid IS NULL
|
||||
@@ -327,8 +350,17 @@ class PostProcessRunner extends BaseRunner
|
||||
$checkSql = '
|
||||
SELECT id
|
||||
FROM releases
|
||||
WHERE categories_id BETWEEN 7000 AND 7999
|
||||
AND bookinfo_id IS NULL
|
||||
WHERE (
|
||||
categories_id BETWEEN 7000 AND 7999
|
||||
OR categories_id = 3030
|
||||
)
|
||||
AND (
|
||||
bookinfo_id IS NULL
|
||||
OR searchname LIKE "N:/NZB%"
|
||||
OR searchname LIKE "N_NZB_%"
|
||||
OR name LIKE "N:/NZB%"
|
||||
OR name LIKE "N_NZB_%"
|
||||
)
|
||||
LIMIT 1';
|
||||
if (count(DB::select($checkSql)) === 0) {
|
||||
$this->headerNone();
|
||||
@@ -336,11 +368,21 @@ class PostProcessRunner extends BaseRunner
|
||||
return;
|
||||
}
|
||||
|
||||
$bucketExpr = $this->guidBucketExpression();
|
||||
$sql = '
|
||||
SELECT DISTINCT LEFT(leftguid, 1) AS id
|
||||
SELECT DISTINCT '.$bucketExpr.' AS id
|
||||
FROM releases
|
||||
WHERE categories_id BETWEEN 7000 AND 7999
|
||||
AND bookinfo_id IS NULL
|
||||
WHERE (
|
||||
categories_id BETWEEN 7000 AND 7999
|
||||
OR categories_id = 3030
|
||||
)
|
||||
AND (
|
||||
bookinfo_id IS NULL
|
||||
OR searchname LIKE "N:/NZB%"
|
||||
OR searchname LIKE "N_NZB_%"
|
||||
OR name LIKE "N:/NZB%"
|
||||
OR name LIKE "N_NZB_%"
|
||||
)
|
||||
LIMIT 16';
|
||||
$queue = DB::select($sql);
|
||||
|
||||
|
||||
@@ -36,10 +36,12 @@ class BookMatchScorer
|
||||
+ (0.07 * $coverScore);
|
||||
}
|
||||
|
||||
return (0.65 * $titleScore)
|
||||
+ (0.15 * $yearScore)
|
||||
+ (0.10 * $publisherScore)
|
||||
+ (0.10 * $coverScore);
|
||||
// When the release has no reliable author, avoid over-trusting
|
||||
// "has cover/publisher" signals for weak title matches.
|
||||
return (0.80 * $titleScore)
|
||||
+ (0.10 * $yearScore)
|
||||
+ (0.05 * $publisherScore)
|
||||
+ (0.05 * $coverScore);
|
||||
}
|
||||
|
||||
public function scoreBookInfo(BookInfo $book, BookParseResult $parsed): float
|
||||
|
||||
Reference in New Issue
Block a user