Update books handling

This commit is contained in:
DariusIII
2026-04-23 14:51:59 +02:00
parent d8b19cb346
commit 891d5f6865
17 changed files with 353 additions and 56 deletions
+10 -6
View File
@@ -517,6 +517,10 @@ if (! function_exists('getReleaseCover')) {
return null;
};
$isPositiveId = static function (mixed $value): bool {
return is_numeric($value) && (int) $value > 0;
};
// Determine cover type and ID based on category
$imdbid = $getValue($release, 'imdbid');
$videos_id = $getValue($release, 'videos_id');
@@ -529,22 +533,22 @@ if (! function_exists('getReleaseCover')) {
if (! empty($imdbid) && imdb_id_is_valid($imdbid)) {
$coverType = 'movies';
$coverId = (string) $imdbid;
} elseif (! empty($videos_id) && $videos_id > 0) {
} elseif ($isPositiveId($videos_id)) {
$coverType = 'tvshows';
$coverId = $videos_id;
} elseif (! empty($musicinfo_id)) {
} elseif ($isPositiveId($musicinfo_id)) {
$coverType = 'music';
$coverId = $musicinfo_id;
} elseif (! empty($consoleinfo_id)) {
} elseif ($isPositiveId($consoleinfo_id)) {
$coverType = 'console';
$coverId = $consoleinfo_id;
} elseif (! empty($bookinfo_id)) {
} elseif ($isPositiveId($bookinfo_id)) {
$coverType = 'book';
$coverId = $bookinfo_id;
} elseif (! empty($gamesinfo_id)) {
} elseif ($isPositiveId($gamesinfo_id)) {
$coverType = 'games';
$coverId = $gamesinfo_id;
} elseif (! empty($anidbid) && $anidbid > 0) {
} elseif ($isPositiveId($anidbid)) {
$coverType = 'anime';
$coverId = $anidbid;
}
+28 -14
View File
@@ -10,6 +10,11 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
class CoverController extends Controller
{
/**
* @var array<int, string>
*/
private const array NUMERIC_ID_TYPES = ['anime', 'book', 'console', 'games', 'music', 'tvshows'];
/**
* Serve cover images from storage
*
@@ -26,6 +31,10 @@ class CoverController extends Controller
abort(404);
}
if (in_array($type, self::NUMERIC_ID_TYPES, true) && $this->isInvalidNumericCoverFilename($filename)) {
return $this->respondWithPlaceholder();
}
// Build the file path
// For preview and sample images, try with _thumb suffix first
if (in_array($type, ['preview', 'sample'], true)) {
@@ -40,20 +49,6 @@ class CoverController extends Controller
$filePath = storage_path("covers/{$type}/{$filename}");
}
} elseif ($type === 'anime') {
// For anime, check if the ID is valid (must be > 0)
// Reject covers for anidbid <= 0 (failed processing, no match, etc.)
if (preg_match('/^(-?\d+)(?:-cover)?\.jpg$/', $filename, $matches)) {
$anidbid = (int) $matches[1];
if ($anidbid <= 0) {
// Return placeholder for invalid IDs
$placeholderPath = public_path('assets/images/no-cover.png');
if (file_exists($placeholderPath)) {
return response()->file($placeholderPath);
}
abort(404);
}
}
// For anime, try the requested filename first, then fall back to old format (without -cover)
$filePath = storage_path("covers/{$type}/{$filename}");
@@ -94,4 +89,23 @@ class CoverController extends Controller
'Cache-Control' => 'public, max-age=31536000', // Cache for 1 year
]);
}
private function isInvalidNumericCoverFilename(string $filename): bool
{
if (preg_match('/^(-?\d+)(?:-cover)?\.jpg$/', $filename, $matches) !== 1) {
return false;
}
return (int) $matches[1] <= 0;
}
private function respondWithPlaceholder(): Response|BinaryFileResponse
{
$placeholderPath = public_path('assets/images/no-cover.png');
if (file_exists($placeholderPath)) {
return response()->file($placeholderPath);
}
abort(404);
}
}
+4 -4
View File
@@ -108,22 +108,22 @@ class DetailsController extends BasePageController
}
$game = '';
if (! empty($data['gamesinfo_id'])) {
if ((int) $data['gamesinfo_id'] > 0) {
$game = (new GamesService)->getGamesInfoById($data['gamesinfo_id']);
}
$mus = '';
if (! empty($data['musicinfo_id'])) {
if ((int) $data['musicinfo_id'] > 0) {
$mus = (new MusicService)->getMusicInfo($data['musicinfo_id']);
}
$book = '';
if (! empty($data['bookinfo_id'])) {
if ((int) $data['bookinfo_id'] > 0) {
$book = (new BookService)->getBookInfo($data['bookinfo_id']);
}
$con = '';
if (! empty($data['consoleinfo_id'])) {
if ((int) $data['consoleinfo_id'] > 0) {
$con = (new ConsoleService)->getConsoleInfo($data['consoleinfo_id']);
}
+1 -1
View File
@@ -48,7 +48,7 @@ class BookResource extends JsonResource
*/
protected function getCoverUrl(): ?string
{
if (! $this->cover) {
if ((int) $this->id <= 0 || ! $this->cover) {
return null;
}
+8 -2
View File
@@ -80,6 +80,10 @@ class BookInfo extends Model
*/
public function getCoverPath(): string
{
if ((int) $this->id <= 0) {
return '';
}
return storage_path('covers/book/'.$this->id.'.jpg');
}
@@ -88,7 +92,9 @@ class BookInfo extends Model
*/
public function hasCoverImage(): bool
{
return file_exists($this->getCoverPath());
$coverPath = $this->getCoverPath();
return $coverPath !== '' && file_exists($coverPath);
}
/**
@@ -96,7 +102,7 @@ class BookInfo extends Model
*/
public function getCoverUrl(): ?string
{
if (! $this->cover || ! $this->hasCoverImage()) {
if ((int) $this->id <= 0 || ! $this->cover || ! $this->hasCoverImage()) {
return null;
}
+76 -7
View File
@@ -10,6 +10,7 @@ use App\Models\BookInfo;
use App\Models\Category;
use App\Models\Release;
use App\Models\Settings;
use App\Services\NameFixing\Extractors\ObfuscatedSubjectExtractor;
use App\Services\Releases\ReleaseBrowseService;
use App\Support\BookMatchScorer;
use App\Support\DTOs\BookParseResult;
@@ -17,6 +18,7 @@ use App\Support\MetadataSearchLookup;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Service class for book data fetching and processing.
@@ -37,6 +39,8 @@ class BookService
public ?BookParseResult $parsedBookResult;
private ObfuscatedSubjectExtractor $obfuscatedSubjectExtractor;
/**
* @throws \Exception
*/
@@ -52,6 +56,7 @@ class BookService
$this->parsedIsbn = null;
$this->parsedBookResult = null;
$this->obfuscatedSubjectExtractor = new ObfuscatedSubjectExtractor;
}
/**
@@ -363,7 +368,10 @@ class BookService
{
$query = Release::query()
->whereNull('bookinfo_id')
->whereBetween('categories_id', [Category::BOOKS_ROOT, Category::BOOKS_UNKNOWN])
->where(static function ($builder): void {
$builder->whereBetween('categories_id', [Category::BOOKS_ROOT, Category::BOOKS_UNKNOWN])
->orWhere('categories_id', Category::MUSIC_AUDIOBOOK);
})
->orderByDesc('postdate')
->limit($this->bookqty);
@@ -377,7 +385,12 @@ class BookService
$this->processBookReleasesHelper(
$query->get(['searchname', 'id', 'categories_id']),
sprintf('%d-%d', Category::BOOKS_ROOT, Category::BOOKS_UNKNOWN)
sprintf(
'%d-%d,%d',
Category::BOOKS_ROOT,
Category::BOOKS_UNKNOWN,
Category::MUSIC_AUDIOBOOK
)
);
}
@@ -459,7 +472,8 @@ class BookService
*/
public function parseTitle(mixed $release_name, mixed $releaseID, mixed $releasetype)
{
$parsed = $this->parseReleaseName((string) $release_name, (string) $releasetype);
$normalizedReleaseName = $this->obfuscatedSubjectExtractor->extract((string) $release_name) ?? (string) $release_name;
$parsed = $this->parseReleaseName($normalizedReleaseName, (string) $releasetype);
$this->parsedBookResult = $parsed;
$this->parsedIsbn = $parsed->isbn;
$releasename = $parsed->title;
@@ -514,6 +528,10 @@ class BookService
public function parseReleaseName(string $releaseName, string $releaseType = 'ebook'): BookParseResult
{
$releaseName = $this->obfuscatedSubjectExtractor->extract($releaseName) ?? $releaseName;
if (preg_match('/"([^"]{3,240})"/', $releaseName, $quotedMatch) === 1) {
$releaseName = $quotedMatch[1];
}
$isbn = $this->extractIsbn($releaseName);
$a = preg_replace('/\d{1,2} \d{1,2} \d{2,4}|(19|20)\d\d|anybody got .+?[a-z]\? |[ ._-](Novel|TIA)([ ._-]|$)|([ \.])HQ([-\. ])|[\(\)\.\-_ ](AVI|AZW3?|DOC|EPUB|LIT|MOBI|NFO|RETAIL|(si)?PDF|RTF|TXT)[\)\]\.\-_ ](?![a-z0-9])|compleet|DAGSTiDNiNGEN|DiRFiX|\+ extra|r?e ?Books?([\.\-_ ]English|ers)?|azw3?|ePu([bp])s?|html|mobi|^NEW[\.\-_ ]|PDF([\.\-_ ]English)?|Please post more|Post description|Proper|Repack(fix)?|[\.\-_ ](Chinese|English|French|German|Italian|Retail|Scan|Swedish|Multilingual)|^R4 |Repost|Skytwohigh|TIA!+|TruePDF|V413HAV|(would someone )?please (re)?post.+? "|with the authors name right/i', '', $releaseName);
$b = preg_replace('/^(As Req |conversion |eq |Das neue Abenteuer \d+|Fixed version( ignore previous post)?|Full |Per Req As Found|(\s+)?R4 |REQ |revised |version |\d+(\s+)?$)|(COMPLETE|INTERNAL|RELOADED| (AZW3|eB|docx|ENG?|exe|FR|Fix|gnv64|MU|NIV|R\d\s+\d{1,2} \d{1,2}|R\d|Req|TTL|UC|v(\s+)?\d))(\s+)?$/i', '', (string) $a);
@@ -538,8 +556,12 @@ class BookService
$author = null;
$title = $normalized;
if (preg_match('/^(?<author>[A-Za-z0-9&\'\.\-\s]{3,80})\s-\s(?<title>.+)$/', $normalized, $matches) === 1) {
$author = trim($matches['author']);
$title = trim($matches['title']);
$candidateAuthor = trim($matches['author']);
$candidateTitle = trim($matches['title']);
if (preg_match('/^(Issue\s+\d+|\d+\w*\s+Edition\b)/i', $candidateTitle) !== 1) {
$author = $candidateAuthor;
$title = $candidateTitle;
}
} elseif (preg_match('/^(?<author>[A-Za-z0-9&\'\.\-\s]{3,80})\s+by\s+(?<title>.+)$/i', $normalized, $matches) === 1) {
$author = trim($matches['author']);
$title = trim($matches['title']);
@@ -549,6 +571,12 @@ class BookService
}
$title = trim((string) preg_replace('/\s+/', ' ', (string) preg_replace('/\((Book|Series)\s*\d+\)/i', '', $title)));
if ($author === null && preg_match('/^(?<title>.+?)[\s._-]+(?<year>(19|20)\d{2})[\s._-]+(?<format>EPUB|MOBI|AZW3?|PDF|FB2|DJVU|LIT)$/i', $normalized, $matches) === 1) {
$title = trim(str_replace(['.', '_'], ' ', $matches['title']));
}
if ($author === null && preg_match('/^(?<title>.+?)\s+Vol\.?\s*\d{1,3}$/i', $title, $matches) === 1) {
$title = trim($matches['title']);
}
$isJunk = false;
$junkPattern = '/^([a-z0-9] )+$|ArtofUsenet|ekiosk|(ebook|mobi).+collection|erotica|Full Video|ImwithJamie|linkoff org|Mega.+pack|^[a-z0-9]+ (?!((January|February|March|April|May|June|July|August|September|O([ck])tober|November|De([cz])ember)))[a-z]+( (ebooks?|The))?$|NY Times|(Book|Massive) Dump|Sexual/i';
@@ -574,8 +602,11 @@ class BookService
$isJunk = true;
}
$isMagazine = 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('/Part \d+/i', $normalized) !== 1;
$isMagazine = (
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('/Part \d+/i', $normalized) !== 1;
return new BookParseResult(
rawName: $releaseName,
@@ -627,19 +658,40 @@ class BookService
$bookId = -2;
$book = null;
if ($bookInfo !== '') {
$wordCount = count(array_filter(preg_split('/\s+/', trim($bookInfo)) ?: []));
if ($wordCount < 3) {
return -2;
}
$parsed = $this->parsedBookResult ?? $this->parseReleaseName($bookInfo);
$candidates = [];
$resolvedSource = null;
if ($isbnDb->isConfigured()) {
if ($isbn !== null && $isbn !== '') {
cli()->info('Fetching data from ISBNdb by ISBN '.$isbn);
$candidate = $isbnDb->findByIsbn($isbn);
if (is_array($candidate)) {
$candidates[] = $candidate;
$resolvedSource = 'isbndb_isbn';
}
}
if ($candidates === []) {
cli()->info('Fetching data from ISBNdb for '.$bookInfo);
$candidates = array_merge($candidates, $isbnDb->searchBooks($bookInfo));
if ($candidates !== []) {
$resolvedSource = 'isbndb_search';
}
}
}
if ($candidates === []) {
if ($isbn !== null && $isbn !== '') {
cli()->info('Fetching data from Google Books by ISBN '.$isbn);
$candidate = $googleBooks->findByIsbn($isbn);
if (is_array($candidate)) {
$candidates[] = $candidate;
$resolvedSource = 'google_books_isbn';
}
}
}
@@ -654,11 +706,17 @@ class BookService
$isbn
)
);
if ($candidates !== []) {
$resolvedSource = 'google_books_search';
}
}
if ($candidates === []) {
cli()->info('Fetching data from iTunes for '.$bookInfo);
$candidates = array_merge($candidates, $this->fetchItunesBookProperties($bookInfo, $itunes));
if ($candidates !== []) {
$resolvedSource = 'itunes_search';
}
}
if ($candidates === []) {
@@ -667,15 +725,26 @@ class BookService
$candidate = $openLibrary->findByIsbn($isbn);
if (is_array($candidate)) {
$candidates[] = $candidate;
$resolvedSource = 'open_library_isbn';
}
}
if ($candidates === []) {
$candidates = array_merge($candidates, $openLibrary->searchBooks($bookInfo));
if ($candidates !== []) {
$resolvedSource = 'open_library_search';
}
}
}
$book = $this->pickBestCandidate($candidates, $parsed, $scorer);
if (is_array($book)) {
Log::debug('Book metadata source resolved', [
'book_info' => $bookInfo,
'source' => $resolvedSource,
'isbn' => $isbn,
]);
}
}
if (empty($book)) {
@@ -8,6 +8,7 @@ use App\Models\Settings;
use App\Models\UsenetGroup;
use App\Services\Categorization\Pipes\AbstractCategorizationPipe;
use App\Services\Categorization\Pipes\CategorizationPassable;
use App\Services\NameFixing\Extractors\ObfuscatedSubjectExtractor;
use App\Services\NameFixing\NzbSplitUnwrapper;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Collection;
@@ -34,11 +35,16 @@ class CategorizationPipeline
protected NzbSplitUnwrapper $nzbSplitUnwrapper;
protected ObfuscatedSubjectExtractor $obfuscatedSubjectExtractor;
/**
* @param iterable<AbstractCategorizationPipe> $pipes
*/
public function __construct(iterable $pipes = [], ?NzbSplitUnwrapper $nzbSplitUnwrapper = null)
{
public function __construct(
iterable $pipes = [],
?NzbSplitUnwrapper $nzbSplitUnwrapper = null,
?ObfuscatedSubjectExtractor $obfuscatedSubjectExtractor = null
) {
/** @phpstan-ignore argument.templateType */
$this->pipes = collect($pipes)
->sortBy(fn (AbstractCategorizationPipe $p) => $p->getPriority());
@@ -46,6 +52,7 @@ class CategorizationPipeline
$this->categorizeForeign = (bool) Settings::settingValue('categorizeforeign');
$this->catWebDL = (bool) Settings::settingValue('catwebdl');
$this->nzbSplitUnwrapper = $nzbSplitUnwrapper ?? new NzbSplitUnwrapper;
$this->obfuscatedSubjectExtractor = $obfuscatedSubjectExtractor ?? new ObfuscatedSubjectExtractor;
}
/**
@@ -75,6 +82,7 @@ class CategorizationPipeline
bool $debug = false
): array {
$releaseName = $this->nzbSplitUnwrapper->unwrap($releaseName) ?? $releaseName;
$releaseName = $this->obfuscatedSubjectExtractor->extract($releaseName) ?? $releaseName;
$groupName = UsenetGroup::whereId($groupId)->value('name') ?? '';
@@ -136,12 +136,16 @@ class BookCategorizer extends AbstractCategorizer
{
$magazines = 'Forbes|Fortune|GQ|National[._ -]Geographic|Newsweek|Vogue|Wired|The[._ -]?Economist|New[._ -]?Yorker|Scientific[._ -]?American|Popular[._ -]?Mechanics|Cosmopolitan|Elle|Esquire|Vanity[._ -]?Fair|Rolling[._ -]?Stone|Entertainment[._ -]?Weekly|People|Playboy';
$hasTitle = preg_match('/\b('.$magazines.')\b/i', $name) === 1;
$hasIssueNumber = preg_match('/(?:^|[._ -])Issue[._ -]?\d{1,4}(?:$|[._ -])/i', $name) === 1;
$hasIssueNumber = preg_match('/(?:^|[._ -])Issue[._ -]?\d{1,4}(?:$|[._ -,])/i', $name) === 1;
$hasDateSignal = preg_match('/\b(?:19|20)\d{2}\b|(?:^|[._ -])(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*[._ -]?(?:19|20)?\d{2}\b/i', $name) === 1;
$hasFrequency = preg_match('/[._ -](Monthly|Weekly|Quarterly|Annual)[._ -]/i', $name) === 1;
$hasIssueStyleTitle = preg_match('/\b([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*)\s*-\s*Issue\s+\d+/i', $name) === 1;
$hasMcnMagazineSignal = preg_match('/\bMCN[._ -](?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|January|February|March|April|June|July|August|September|October|November|December)[._ -]\d{1,2}[._ -](?:19|20)\d{2}\b/i', $name) === 1
&& preg_match('/\bMAGAZINE\b/i', $name) === 1;
if (($hasFrequency && ($hasIssueNumber || $hasDateSignal || $hasTitle)) ||
($hasIssueNumber && ($hasDateSignal || $hasTitle))) {
($hasIssueNumber && ($hasDateSignal || $hasTitle || $hasIssueStyleTitle)) ||
$hasMcnMagazineSignal) {
return $this->matched(Category::BOOKS_MAGAZINES, 0.9, 'magazine_frequency');
}
if ($hasTitle) {
@@ -173,6 +177,9 @@ class BookCategorizer extends AbstractCategorizer
if (preg_match('/\b(E-?book|Kindle|Kobo|Nook)\b/i', $name)) {
return $this->matched(Category::BOOKS_EBOOK, 0.8, 'ebook_platform');
}
if ($this->hasBookCorroborator($name) && preg_match('/\b(19|20)\d{2}\b/', $name) === 1) {
return $this->matched(Category::BOOKS_EBOOK, 0.6, 'ebook_book_signals');
}
return null;
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Services\NameFixing\Extractors;
final class ObfuscatedSubjectExtractor
{
/**
* @var list<string>
*/
private const array PREFIX_PATTERNS = [
'/^\s*N:\/NZB\s*\[\d+\/\d+\]\s*-\s*/i',
'/^\s*\[[^\]]{2,80}\]\s*/',
'/^\s*(?:re\s*)?posted\s+by\s+[^-]{2,120}\s*-\s*/i',
];
/**
* @var list<string>
*/
private const array TRAILING_ARCHIVE_PATTERNS = [
'/\.part\d+\.rar$/i',
'/\.r\d{2,4}$/i',
'/\.7z\.\d{2,4}$/i',
'/\.7z$/i',
'/\.rar$/i',
'/\.par2?$/i',
'/\.vol\d+[+\-]\d+\.par2?$/i',
'/\.\d{3}$/',
];
public function extract(string $value): ?string
{
$original = trim($value);
if ($original === '') {
return null;
}
$normalized = $original;
$looksObfuscated = false;
foreach (self::PREFIX_PATTERNS as $pattern) {
$updated = preg_replace($pattern, '', $normalized) ?? $normalized;
if ($updated !== $normalized) {
$looksObfuscated = true;
$normalized = $updated;
}
}
if (preg_match('/"([^"]{3,240})"/', $normalized, $quoted) === 1) {
$normalized = $quoted[1];
$looksObfuscated = true;
} elseif (preg_match("/'([^']{3,240})'/", $normalized, $quoted) === 1) {
$normalized = $quoted[1];
$looksObfuscated = true;
}
if ($looksObfuscated) {
foreach (self::TRAILING_ARCHIVE_PATTERNS as $pattern) {
$normalized = preg_replace($pattern, '', $normalized) ?? $normalized;
}
}
$normalized = str_replace(['_', '+'], [' ', ' '], $normalized);
$normalized = preg_replace('/\.(?=[A-Za-z0-9])/', ' ', $normalized) ?? $normalized;
$normalized = preg_replace('/\s{2,}/', ' ', $normalized) ?? $normalized;
$normalized = trim($normalized, " \t\n\r\0\x0B\"'`-_.");
if ($normalized === '' || $normalized === $original) {
return null;
}
return $normalized;
}
}
+6 -2
View File
@@ -167,8 +167,12 @@ class FileNameCleaner
// Remove common video file extensions
$t = preg_replace('/\.(mkv|avi|mp4|m4v|mpg|mpeg|wmv|flv|mov|ts|vob|iso|divx)$/i', '', $t) ?? $t;
// Remove archive and metadata file extensions
$t = preg_replace('/\.(par2?|nfo|sfv|nzb|rar|zip|7z|gz|tar|bz2|xz|r\d{2,3}|\d{3}|pkg|exe|msi|jpe?g|png|gif|bmp)$/i', '', $t) ?? $t;
// Remove archive, metadata, and common document/file extensions
$t = preg_replace(
'/\.(par2?|nfo|sfv|nzb|rar|zip|7z|gz|tar|bz2|xz|r\d{2,3}|\d{3}|pkg|exe|msi|jpe?g|png|gif|bmp|pdf|epub|mobi|azw3?|djvu|cbr|cbz|fb2|lit|prc|opf|txt|log)$/i',
'',
$t
) ?? $t;
// Remove common trailing segment markers
$t = preg_replace('/[.\-_ ](?:part|vol|r)\d+(?:\+\d+)?$/i', '', $t) ?? $t;
+1 -1
View File
@@ -123,7 +123,7 @@ final class PostProcessService
}
/**
* Process book releases using Amazon.
* Process book releases using ISBNdb, Google Books, iTunes, and Open Library.
*
* @param string $groupID Optional group ID filter
* @param string $guidChar Optional GUID character filter