Update book processing further

This commit is contained in:
DariusIII
2026-04-23 17:01:28 +02:00
parent 891d5f6865
commit b20e01a009
15 changed files with 594 additions and 30 deletions
+151
View File
@@ -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.
*/
+121 -9
View File
@@ -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;
+52 -10
View File
@@ -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);
+6 -4
View File
@@ -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