From 677ba50f93796187b2fa6b5590a402770a6fdc62 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Fri, 26 Dec 2025 10:25:34 +0100 Subject: [PATCH] Fix search and create nzb services --- Blacklight/NZBImport.php | 9 +- Blacklight/Nfo.php | 22 +- app/Console/Commands/CleanNZB.php | 6 +- .../Commands/NntmuxRemoveBadReleases.php | 4 +- .../Commands/ReleasesFixNamesGroup.php | 14 +- app/Extensions/helper/helpers.php | 6 +- app/Http/Controllers/Api/ApiController.php | 1 - .../Controllers/Api/FileListApiController.php | 10 +- app/Http/Controllers/GetNzbController.php | 4 +- app/Models/UsenetGroup.php | 4 +- .../AdditionalProcessingServiceProvider.php | 8 +- .../AdditionalProcessing/NzbContentParser.php | 14 +- .../ReleaseFileManager.php | 6 +- app/Services/NameFixing/NameFixingService.php | 4 +- .../Services/Nzb/NzbContentsService.php | 252 +++++++------- app/Services/Nzb/NzbParserService.php | 310 ++++++++++++++++++ .../Services/Nzb/NzbService.php | 190 ++++------- app/Services/ReleaseCreationService.php | 4 +- app/Services/ReleaseProcessingService.php | 10 +- app/Services/ReleaseRemoverService.php | 8 +- .../Releases/ReleaseManagementService.php | 10 +- .../Releases/ReleaseSearchService.php | 75 +++-- misc/testing/Dev/clean_nzbs.php | 10 +- misc/testing/NZB/nzb-reorg.php | 6 +- misc/testing/PostProc/check_previews.php | 4 +- 25 files changed, 610 insertions(+), 381 deletions(-) rename Blacklight/NZBContents.php => app/Services/Nzb/NzbContentsService.php (51%) mode change 100755 => 100644 create mode 100644 app/Services/Nzb/NzbParserService.php rename Blacklight/NZB.php => app/Services/Nzb/NzbService.php (61%) mode change 100755 => 100644 diff --git a/Blacklight/NZBImport.php b/Blacklight/NZBImport.php index 3132b26d2..1b5f4f38c 100755 --- a/Blacklight/NZBImport.php +++ b/Blacklight/NZBImport.php @@ -7,6 +7,7 @@ use App\Models\Settings; use App\Models\UsenetGroup; use App\Services\BlacklistService; use App\Services\Categorization\CategorizationService; +use App\Services\Nzb\NzbService; use App\Services\ReleaseCleaningService; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\File; @@ -55,7 +56,7 @@ class NZBImport */ public mixed $echoCLI; - public NZB $nzb; + public NzbService $nzb; /** * @var string the MD5 hash of the first segment Message-ID of the NZB @@ -69,7 +70,7 @@ class NZBImport $this->echoCLI = config('nntmux.echocli'); $this->blacklistService = new BlacklistService; $this->category = new CategorizationService(); - $this->nzb = new NZB; + $this->nzb = app(NzbService::class); $this->releaseCleaner = new ReleaseCleaningService; $this->colorCli = new ColorCLI; $this->crossPostt = Settings::settingValue('crossposttime') !== '' ? Settings::settingValue('crossposttime') : 2; @@ -163,7 +164,7 @@ class NZBImport if ($inserted) { // Try to copy the NZB to the NZB folder. - $path = $this->nzb->getNZBPath($this->relGuid, 0, true); + $path = $this->nzb->getNzbPath($this->relGuid, 0, true); // Try to compress the NZB file in the NZB folder. $fp = gzopen($path, 'w5'); @@ -393,7 +394,7 @@ class NZBImport 'categories_id' => $determinedCategory['categories_id'], 'isrenamed' => $renamed, 'predb_id' => 0, - 'nzbstatus' => NZB::NZB_ADDED, + 'nzbstatus' => NzbService::NZB_ADDED, 'ishashed' => 0, ] ); diff --git a/Blacklight/Nfo.php b/Blacklight/Nfo.php index 6c7d32691..b891516f3 100755 --- a/Blacklight/Nfo.php +++ b/Blacklight/Nfo.php @@ -9,6 +9,7 @@ use App\Models\ReleaseNfo; use App\Models\Settings; use App\Models\UsenetGroup; use App\Services\NNTP\NNTPService; +use App\Services\Nzb\NzbContentsService; use App\Services\PostProcessService; use dariusiii\rarinfo\Par2Info; use dariusiii\rarinfo\SfvInfo; @@ -478,16 +479,11 @@ class Nfo } if ($release->completion === 0) { - $nzbContents = new NZBContents( - [ - 'Echo' => $this->echo, - 'NNTP' => $nntp, - 'Nfo' => $this, - 'Settings' => null, - 'PostProcess' => app(PostProcessService::class), - ] - ); - $nzbContents->parseNZB($release->guid, $release->id, $release->guid); + $nzbContentsService = app(NzbContentsService::class); + $nzbContentsService->setNntp($nntp); + $nzbContentsService->setNfo($this); + $nzbContentsService->setEchoOutput($this->echo); + $nzbContentsService->parseNzb($release->guid, $release->id, $release->groups_id ?? 0); } return true; @@ -534,12 +530,14 @@ class Nfo } // Process each release - $nzbContents = new NZBContents(['NNTP' => $nntp, 'Nfo' => $this]); + $nzbContentsService = app(NzbContentsService::class); + $nzbContentsService->setNntp($nntp); + $nzbContentsService->setNfo($this); foreach ($releases as $release) { try { $groupName = UsenetGroup::getNameByID($release['groups_id']); - $fetchedBinary = $nzbContents->getNfoFromNZB($release['guid'], $release['id'], $release['groups_id'], $groupName); + $fetchedBinary = $nzbContentsService->getNfoFromNzb($release['guid'], $release['id'], $release['groups_id'], $groupName); if ($fetchedBinary !== false) { DB::beginTransaction(); diff --git a/app/Console/Commands/CleanNZB.php b/app/Console/Commands/CleanNZB.php index d81a6d767..c794c2de4 100644 --- a/app/Console/Commands/CleanNZB.php +++ b/app/Console/Commands/CleanNZB.php @@ -3,9 +3,9 @@ namespace App\Console\Commands; use App\Models\Release; +use App\Services\Nzb\NzbService; use App\Services\ReleaseImageService; use App\Services\Releases\ReleaseManagementService; -use Blacklight\NZB; use Illuminate\Console\Command; use Illuminate\Support\Collection; use Illuminate\Support\Facades\File; @@ -79,7 +79,7 @@ class CleanNZB extends Command private function GetReleasesWithNoNZBOnDisk($delete = false) { // Setup - $nzb = new NZB; + $nzb = app(NzbService::class); $releaseManagement = app(ReleaseManagementService::class); $checked = $deleted = 0; @@ -91,7 +91,7 @@ class CleanNZB extends Command echo 'Total done: '.$checked."\r"; foreach ($releases as $r) { - if (! $nzb->NZBPath($r->guid)) { + if (! $nzb->nzbPath($r->guid)) { if ($delete) { $releaseManagement->deleteSingleWithService(['g' => $r->guid, 'i' => $r->id], $nzb, new ReleaseImageService); } diff --git a/app/Console/Commands/NntmuxRemoveBadReleases.php b/app/Console/Commands/NntmuxRemoveBadReleases.php index 1ca06b7d2..645117aad 100644 --- a/app/Console/Commands/NntmuxRemoveBadReleases.php +++ b/app/Console/Commands/NntmuxRemoveBadReleases.php @@ -5,8 +5,8 @@ namespace App\Console\Commands; use App\Facades\Search; use App\Models\Release; use App\Models\ReleaseFile; +use App\Services\Nzb\NzbService; use App\Services\ReleaseImageService; -use Blacklight\NZB; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; @@ -47,7 +47,7 @@ class NntmuxRemoveBadReleases extends Command // Select releases with password status -2 and smaller and delete them. Also delete the files from the filesystem. $badReleases = Release::query()->where('passwordstatus', '<=', -2)->get(); foreach ($badReleases as $badRelease) { - $nzbPath = (new NZB)->getNZBPath($badRelease->guid); + $nzbPath = app(NzbService::class)->getNzbPath($badRelease->guid); File::delete($nzbPath); (new ReleaseImageService)->delete($badRelease->guid); // Delete from search index diff --git a/app/Console/Commands/ReleasesFixNamesGroup.php b/app/Console/Commands/ReleasesFixNamesGroup.php index cdad3a5f4..90c86aa1c 100644 --- a/app/Console/Commands/ReleasesFixNamesGroup.php +++ b/app/Console/Commands/ReleasesFixNamesGroup.php @@ -8,10 +8,10 @@ use App\Models\Category; use App\Models\Predb; use App\Models\Release; use App\Services\NameFixing\NameFixingService; +use App\Services\Nzb\NzbContentsService; use App\Services\PostProcessService; use App\Services\NNTP\NNTPService; use Blacklight\Nfo; -use Blacklight\NZBContents; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; @@ -226,17 +226,15 @@ class ReleasesFixNamesGroup extends Command $this->warn($errorMessage); } else { $Nfo = new Nfo(); - $nzbcontents = new NZBContents([ - 'Echo' => false, - 'NNTP' => $nntp, - 'Nfo' => $Nfo, - 'PostProcess' => app(PostProcessService::class), - ]); + $nzbcontents = app(NzbContentsService::class); + $nzbcontents->setNntp($nntp); + $nzbcontents->setNfo($Nfo); + $nzbcontents->setEchoOutput(false); } } if (isset($nzbcontents)) { - $nzbcontents->checkPAR2($release->guid, $release->releases_id, $release->groups_id, 1, 1); + $nzbcontents->checkPar2($release->guid, $release->releases_id, $release->groups_id, 1, 1); } } diff --git a/app/Extensions/helper/helpers.php b/app/Extensions/helper/helpers.php index f55097ed0..4d42647a0 100644 --- a/app/Extensions/helper/helpers.php +++ b/app/Extensions/helper/helpers.php @@ -3,7 +3,7 @@ use App\Models\Country as CountryModel; use App\Models\Release; use App\Models\XxxInfo; -use Blacklight\NZB; +use App\Services\Nzb\NzbService; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Cookie\SetCookie; @@ -299,10 +299,10 @@ if (! function_exists('getStreamingZip')) { */ function getStreamingZip(array $guids = []): STS\ZipStream\Builder { - $nzb = new NZB; + $nzb = app(NzbService::class); $zipped = ZipStream::create(now()->format('Ymdhis').'.zip'); foreach ($guids as $guid) { - $nzbPath = $nzb->NZBPath($guid); + $nzbPath = $nzb->nzbPath($guid); if ($nzbPath) { $nzbContents = unzipGzipFile($nzbPath); if ($nzbContents) { diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index 6fed59b55..205c717da 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -14,7 +14,6 @@ use App\Models\UserDownload; use App\Models\UserRequest; use App\Services\Releases\ReleaseBrowseService; use App\Services\Releases\ReleaseSearchService; -use Blacklight\NZB; use Illuminate\Contracts\Foundation\Application; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; diff --git a/app/Http/Controllers/Api/FileListApiController.php b/app/Http/Controllers/Api/FileListApiController.php index 40a97d5c2..0875402f9 100644 --- a/app/Http/Controllers/Api/FileListApiController.php +++ b/app/Http/Controllers/Api/FileListApiController.php @@ -4,7 +4,8 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Release; -use Blacklight\NZB; +use App\Services\Nzb\NzbParserService; +use App\Services\Nzb\NzbService; use Illuminate\Http\JsonResponse; class FileListApiController extends Controller @@ -14,14 +15,15 @@ class FileListApiController extends Controller */ public function getFileList(string $guid): JsonResponse { - $nzb = new NZB; + $nzb = app(NzbService::class); + $nzbParser = app(NzbParserService::class); $rel = Release::getByGuid($guid); if (! $rel) { return response()->json(['error' => 'Release not found'], 404); } - $nzbpath = $nzb->NZBPath($guid); + $nzbpath = $nzb->nzbPath($guid); if (! file_exists($nzbpath)) { return response()->json(['error' => 'NZB file not found'], 404); @@ -31,7 +33,7 @@ class FileListApiController extends Controller @readgzfile($nzbpath); $nzbfile = ob_get_clean(); - $files = $nzb->nzbFileList($nzbfile); + $files = $nzbParser->parseNzbFileList($nzbfile); return response()->json([ 'release' => [ diff --git a/app/Http/Controllers/GetNzbController.php b/app/Http/Controllers/GetNzbController.php index e0617e223..414fb07be 100644 --- a/app/Http/Controllers/GetNzbController.php +++ b/app/Http/Controllers/GetNzbController.php @@ -6,7 +6,7 @@ use App\Models\Release; use App\Models\User; use App\Models\UserDownload; use App\Models\UsersRelease; -use Blacklight\NZB; +use App\Services\Nzb\NzbService; use Exception; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Routing\ResponseFactory; @@ -251,7 +251,7 @@ class GetNzbController extends BasePageController string $releaseId ) { // Get NZB file path and validate - $nzbPath = (new NZB)->getNZBPath($releaseId); + $nzbPath = app(NzbService::class)->getNzbPath($releaseId); if (! File::exists($nzbPath)) { return showApiError(300, 'NZB file not found!'); } diff --git a/app/Models/UsenetGroup.php b/app/Models/UsenetGroup.php index 77682125e..cf3a91ccb 100644 --- a/app/Models/UsenetGroup.php +++ b/app/Models/UsenetGroup.php @@ -2,10 +2,10 @@ namespace App\Models; +use App\Services\Nzb\NzbService; use App\Services\ReleaseImageService; use App\Services\NNTP\NNTPService; use Blacklight\ColorCLI; -use Blacklight\NZB; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -400,7 +400,7 @@ class UsenetGroup extends Model $res->get(); $releaseManagement = app(\App\Services\Releases\ReleaseManagementService::class); - $nzb = new NZB; + $nzb = app(NzbService::class); $releaseImage = new ReleaseImageService; foreach ($res as $row) { $releaseManagement->deleteSingleWithService( diff --git a/app/Providers/AdditionalProcessingServiceProvider.php b/app/Providers/AdditionalProcessingServiceProvider.php index 4ead132e1..f30dab19f 100644 --- a/app/Providers/AdditionalProcessingServiceProvider.php +++ b/app/Providers/AdditionalProcessingServiceProvider.php @@ -11,11 +11,12 @@ use App\Services\AdditionalProcessing\NzbContentParser; use App\Services\AdditionalProcessing\ReleaseFileManager; use App\Services\AdditionalProcessing\UsenetDownloadService; use App\Services\Categorization\CategorizationService; +use App\Services\Nzb\NzbParserService; +use App\Services\Nzb\NzbService; use App\Services\ReleaseImageService; use App\Services\TempWorkspaceService; use App\Services\NameFixing\NameFixingService; use Blacklight\Nfo; -use Blacklight\NZB; use App\Services\ReleaseExtraService; use Illuminate\Support\ServiceProvider; @@ -50,7 +51,8 @@ class AdditionalProcessingServiceProvider extends ServiceProvider $this->app->singleton(NzbContentParser::class, function ($app) { $config = $app->make(ProcessingConfiguration::class); return new NzbContentParser( - new NZB(), + $app->make(NzbService::class), + $app->make(NzbParserService::class), $config->debugMode, $config->echoCLI ); @@ -77,7 +79,7 @@ class AdditionalProcessingServiceProvider extends ServiceProvider $app->make(ReleaseExtraService::class), new ReleaseImageService(), new Nfo(), - new NZB(), + $app->make(NzbService::class), new NameFixingService() ); }); diff --git a/app/Services/AdditionalProcessing/NzbContentParser.php b/app/Services/AdditionalProcessing/NzbContentParser.php index e8a84f5cf..528b9ce65 100644 --- a/app/Services/AdditionalProcessing/NzbContentParser.php +++ b/app/Services/AdditionalProcessing/NzbContentParser.php @@ -2,7 +2,8 @@ namespace App\Services\AdditionalProcessing; -use Blacklight\NZB; +use App\Services\Nzb\NzbParserService; +use App\Services\Nzb\NzbService; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Log; @@ -13,7 +14,8 @@ use Illuminate\Support\Facades\Log; class NzbContentParser { public function __construct( - private readonly NZB $nzb, + private readonly NzbService $nzb, + private readonly NzbParserService $nzbParser, private readonly bool $debugMode = false, private readonly bool $echoCLI = false ) {} @@ -26,7 +28,7 @@ class NzbContentParser */ public function parseNzb(string $guid): array { - $nzbPath = $this->nzb->NZBPath($guid); + $nzbPath = $this->nzb->nzbPath($guid); if ($nzbPath === false) { return ['contents' => [], 'error' => 'NZB not found for GUID: '.$guid]; } @@ -41,12 +43,12 @@ class NzbContentParser } // Get a list of files in the NZB - $fileList = $this->nzb->nzbFileList($nzbContents, ['no-file-key' => false, 'strip-count' => true]); + $fileList = $this->nzbParser->parseNzbFileList($nzbContents, ['no-file-key' => false, 'strip-count' => true]); if (count($fileList) === 0) { // Attempt repair if initial parse yielded no files $repaired = $this->repairNzb($nzbContents, $nzbPath, $guid); if ($repaired !== null) { - $fileList = $this->nzb->nzbFileList($repaired, ['no-file-key' => false, 'strip-count' => true]); + $fileList = $this->nzbParser->parseNzbFileList($repaired, ['no-file-key' => false, 'strip-count' => true]); } if (count($fileList) === 0) { return ['contents' => [], 'error' => 'NZB is potentially broken for GUID: '.$guid]; @@ -257,7 +259,7 @@ class NzbContentParser */ public function getNzbPath(string $guid): string|false { - return $this->nzb->NZBPath($guid); + return $this->nzb->nzbPath($guid); } } diff --git a/app/Services/AdditionalProcessing/ReleaseFileManager.php b/app/Services/AdditionalProcessing/ReleaseFileManager.php index d5032672d..003c8ae2e 100644 --- a/app/Services/AdditionalProcessing/ReleaseFileManager.php +++ b/app/Services/AdditionalProcessing/ReleaseFileManager.php @@ -6,6 +6,7 @@ use App\Models\MediaInfo as MediaInfoModel; use App\Models\Predb; use App\Models\Release; use App\Models\ReleaseFile; +use App\Services\Nzb\NzbService; use App\Services\ReleaseImageService; use App\Services\Releases\ReleaseBrowseService; use App\Services\AdditionalProcessing\Config\ProcessingConfiguration; @@ -14,7 +15,6 @@ use App\Services\NameFixing\NameFixingService; use App\Services\NameFixing\ReleaseUpdateService; use App\Services\NNTP\NNTPService; use Blacklight\Nfo; -use Blacklight\NZB; use App\Services\ReleaseExtraService; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Support\Carbon; @@ -33,7 +33,7 @@ class ReleaseFileManager private readonly ReleaseExtraService $releaseExtra, private readonly ReleaseImageService $releaseImage, private readonly Nfo $nfo, - private readonly NZB $nzb, + private readonly NzbService $nzb, private readonly NameFixingService $nameFixingService ) {} @@ -199,7 +199,7 @@ class ReleaseFileManager // Delete NZB file try { - $nzbPath = $this->nzb->NZBPath($guid); + $nzbPath = $this->nzb->nzbPath($guid); if ($nzbPath && File::exists($nzbPath)) { File::delete($nzbPath); } diff --git a/app/Services/NameFixing/NameFixingService.php b/app/Services/NameFixing/NameFixingService.php index e0fab2489..7ccc0c18b 100644 --- a/app/Services/NameFixing/NameFixingService.php +++ b/app/Services/NameFixing/NameFixingService.php @@ -943,10 +943,10 @@ class NameFixingService if ($total > 0) { $this->_totalReleases = $total; $this->colorCLI->info(number_format($total) . ' releases to process.'); - $nzbContents = new \Blacklight\NZBContents(); + $nzbContentsService = app(\App\Services\Nzb\NzbContentsService::class); foreach ($releases as $release) { - if ($nzbContents->checkPAR2($release->guid, $release->releases_id, $release->groups_id, (int) $nameStatus, (int) $show)) { + if ($nzbContentsService->checkPar2($release->guid, $release->releases_id, $release->groups_id, (int) $nameStatus, (int) $show)) { $this->updateService->fixed++; } diff --git a/Blacklight/NZBContents.php b/app/Services/Nzb/NzbContentsService.php old mode 100755 new mode 100644 similarity index 51% rename from Blacklight/NZBContents.php rename to app/Services/Nzb/NzbContentsService.php index 02dd1c1c6..d77630e66 --- a/Blacklight/NZBContents.php +++ b/app/Services/Nzb/NzbContentsService.php @@ -2,43 +2,51 @@ declare(strict_types=1); -namespace Blacklight; +namespace App\Services\Nzb; use App\Models\Release; use App\Models\Settings; use App\Services\NNTP\NNTPService; use App\Services\PostProcessService; +use Blacklight\Nfo; /** - * Gets information contained within the NZB. - * - * Class NZBContents + * Service for processing NZB contents - extracting NFO files, PAR2 information, + * and calculating release completion from NZB files. */ -class NZBContents +class NzbContentsService { + protected NzbService $nzbService; + + protected NzbParserService $parserService; + protected NNTPService $nntp; protected Nfo $nfo; protected PostProcessService $postProcessService; - protected NZB $nzb; + protected bool $lookupPar2; - protected bool $lookuppar2; + protected bool $echoOutput; - protected bool $echooutput; + protected bool $alternateNntp; - protected bool $alternateNNTP; - - public function __construct() - { - $this->echooutput = (bool) config('nntmux.echocli'); - $this->nntp = new NNTPService(); - $this->nfo = new Nfo(); - $this->postProcessService = app(PostProcessService::class); - $this->nzb = new NZB(); - $this->lookuppar2 = (int) Settings::settingValue('lookuppar2') === 1; - $this->alternateNNTP = (bool) config('nntmux_nntp.use_alternate_nntp_server'); + public function __construct( + ?NzbService $nzbService = null, + ?NzbParserService $parserService = null, + ?NNTPService $nntp = null, + ?Nfo $nfo = null, + ?PostProcessService $postProcessService = null + ) { + $this->echoOutput = (bool) config('nntmux.echocli'); + $this->nzbService = $nzbService ?? app(NzbService::class); + $this->parserService = $parserService ?? app(NzbParserService::class); + $this->nntp = $nntp ?? new NNTPService(); + $this->nfo = $nfo ?? new Nfo(); + $this->postProcessService = $postProcessService ?? app(PostProcessService::class); + $this->lookupPar2 = (int) Settings::settingValue('lookuppar2') === 1; + $this->alternateNntp = (bool) config('nntmux_nntp.use_alternate_nntp_server'); } /** @@ -52,14 +60,14 @@ class NZBContents * * @throws \Exception If NNTP operations fail. */ - public function getNfoFromNZB(string $guid, int $relID, int $groupID, string $groupName): string|false + public function getNfoFromNzb(string $guid, int $relID, int $groupID, string $groupName): string|false { // Step 1: Attempt to find a potential NFO message ID - $messageID = $this->parseNZB($guid, $relID, $groupID, true); + $messageID = $this->parseNzb($guid, $relID, $groupID, true); // If no NFO message ID found if ($messageID === false || ! isset($messageID['id'])) { - if ($this->echooutput) { + if ($this->echoOutput) { echo '-'; } // Make sure we set status to NFO_NONFO @@ -69,13 +77,13 @@ class NZBContents } // Step 2: Attempt to download the potential NFO content - $fetchedBinary = $this->nntp->getMessages($groupName, $messageID['id'], $this->alternateNNTP); + $fetchedBinary = $this->nntp->getMessages($groupName, $messageID['id'], $this->alternateNntp); // Check if download failed if ($this->nntp->isError($fetchedBinary)) { // NFO download failed, decrement attempts to allow retries Release::query()->where('id', $relID)->decrement('nfostatus'); - if ($this->echooutput) { + if ($this->echoOutput) { echo 'f'; } @@ -85,7 +93,7 @@ class NZBContents // Step 3: Verify if the downloaded content is actually an NFO file if ($this->nfo->isNFO($fetchedBinary, $guid)) { // NFO verification successful - if ($this->echooutput) { + if ($this->echoOutput) { // Show if it was found via explicit name (+) or potentially hidden (*) echo $messageID['hidden'] === false ? '+' : '*'; } @@ -94,7 +102,7 @@ class NZBContents } // Step 4: Handle verification failure - not a valid NFO - if ($this->echooutput) { + if ($this->echoOutput) { echo '-'; } Release::query()->where('id', $relID)->update(['nfostatus' => Nfo::NFO_NONFO]); @@ -105,8 +113,6 @@ class NZBContents /** * Gets the completion from the NZB, optionally looks if there is an NFO/PAR2 file. * - * This version includes improved regex for PAR2 file detection. - * * @param string $guid The release GUID. * @param int $relID The release ID. * @param int $groupID The group ID. @@ -115,7 +121,7 @@ class NZBContents * * @throws \Exception If NNTP operations fail. */ - public function parseNZB($guid, $relID, $groupID, bool $nfoCheck = false): bool|array + public function parseNzb(string $guid, int $relID, int $groupID, bool $nfoCheck = false): bool|array { $nzbFile = $this->loadNzb($guid); if ($nzbFile === false) { @@ -124,8 +130,8 @@ class NZBContents $messageID = $hiddenID = ''; $actualParts = $artificialParts = 0; - // Initialize foundPAR2 based on settings; if lookuppar2 is false, we don't need to find one. - $foundPAR2 = $this->lookuppar2 === false; + // Initialize foundPAR2 based on settings; if lookupPar2 is false, we don't need to find one. + $foundPAR2 = $this->lookupPar2 === false; // Initialize NFO flags based on whether we are checking for NFOs. $foundNFO = $hiddenNFO = $nfoCheck === false; $nfoMessageId = null; // Store potential NFO message ID here @@ -143,28 +149,16 @@ class NZBContents } $subject = (string) $nzbContents->attributes()->subject; - if (preg_match('/(?:[(\[])?(\d+)[\/)\\]](\d+)[)\]]?$/', $subject, $parts)) { - // Improve artificial parts calculation robustness (e.g., "[15/20]", "(15/20)") - if (isset($parts[2]) && (int) $parts[2] > 0) { - // Use the total count from the subject if available and seems valid - $artificialParts += (int) $parts[2]; - } - } elseif (preg_match('/(\d+)\)$/', $subject, $parts)) { - // Fallback to original simple check if the more robust one fails - $artificialParts += (int) $parts[1]; - } + + // Calculate artificial parts from subject + $artificialParts += $this->parserService->extractPartsTotal($subject); // --- NFO Detection --- // Check for explicit NFO files first (with enhanced patterns) if ($nfoCheck && ! $foundNFO && isset($firstSegmentId)) { - // Standard NFO extensions - if (preg_match('/\.\b(nfo|diz|info?)\b(?![.-])/i', $subject)) { - $nfoMessageId = ['hidden' => false, 'id' => $firstSegmentId, 'priority' => 1]; - $foundNFO = true; - } - // Alternative NFO naming patterns (group-specific or obfuscated) - elseif (preg_match('/(?:^|["\s])(?:file(?:_?id)?|readme|release|info(?:rmation)?|about|desc(?:ription)?|notes?|read\.?me|00-|000-|0-|_-_).*?\.(?:txt|nfo|diz)(?:["\s]|$)/i', $subject)) { - $nfoMessageId = ['hidden' => false, 'id' => $firstSegmentId, 'priority' => 2]; + $nfoDetection = $this->parserService->detectNfoFile($subject); + if ($nfoDetection !== false) { + $nfoMessageId = ['hidden' => $nfoDetection['hidden'], 'id' => $firstSegmentId, 'priority' => $nfoDetection['priority']]; $foundNFO = true; } } @@ -172,67 +166,16 @@ class NZBContents // Check for potential "hidden" NFOs with improved detection // Only consider this if an explicit NFO wasn't found yet if ($nfoCheck && ! $foundNFO && ! $hiddenNFO && isset($firstSegmentId)) { - $isHiddenNfoCandidate = false; - - // Pattern 1: Single segment files with (1/1) - if ($segmentCountInFile === 1 && preg_match('/\(1\/1\)$/i', $subject)) { - $isHiddenNfoCandidate = true; - } - - // Pattern 2: Small segment count (1-2) with NFO-like names but no extension - if (! $isHiddenNfoCandidate && $segmentCountInFile <= 2 && preg_match('/(?:^|["\s])(?:nfo|info|readme|release|file_?id|about)(?:["\s]|$)/i', $subject)) { - $isHiddenNfoCandidate = true; - } - - // Pattern 3: Scene-style NFO naming (group-release.nfo without extension visible) - if (! $isHiddenNfoCandidate && $segmentCountInFile === 1 && preg_match('/^[a-z0-9._-]+["\s]*\(1\/1\)/i', $subject)) { - // Check for scene-like naming pattern - if (preg_match('/^[a-z0-9]+[._-][a-z0-9._-]+["\s]*\(1\/1\)/i', $subject)) { - $isHiddenNfoCandidate = true; - } - } - - // Pattern 4: Very small files (NFOs are typically small) - // Files described as very small in bytes could be NFOs - if (! $isHiddenNfoCandidate && $segmentCountInFile === 1 && preg_match('/yEnc\s*\(\d+\)\s*\[1\/1\]/i', $subject)) { - $isHiddenNfoCandidate = true; - } - - if ($isHiddenNfoCandidate) { - // Enhanced exclusion: check if it's NOT likely another common file type - $excludedExtensions = '/\.(?:' . - // Executables - 'exe|com|bat|cmd|scr|dll|msi|pkg|deb|rpm|apk|ipa|app|' . - // Archives - 'zip|rar|[rst]\d{2}|7z|ace|tar|gz|bz2|xz|lzma|cab|iso|bin|cue|img|mdf|nrg|dmg|vhd|' . - // Audio - 'mp3|flac|ogg|aac|wav|wma|m4a|opus|ape|wv|mpc|' . - // Video - 'avi|mkv|mp4|mov|wmv|mpg|mpeg|ts|vob|m2ts|webm|flv|ogv|divx|xvid|' . - // Images - 'jpg|jpeg|png|gif|bmp|tif|tiff|psd|webp|svg|ico|raw|cr2|nef|' . - // Documents - 'pdf|doc|docx|xls|xlsx|ppt|pptx|odt|ods|odp|rtf|epub|mobi|azw|' . - // Code - 'html|htm|css|js|php|py|java|c|cpp|h|cs|sql|json|xml|yml|yaml|' . - // Data - 'db|dbf|mdb|accdb|sqlite|csv|' . - // Verification - 'par2?|sfv|md5|sha1|sha256|sha512|crc|' . - // Misc - 'url|lnk|cfg|ini|inf|sys|tmp|bak|log|srt|sub|idx|ass|ssa|vtt' . - ')\b/i'; - - if (! preg_match($excludedExtensions, $subject)) { - $nfoMessageId = ['hidden' => true, 'id' => $firstSegmentId, 'priority' => 10]; - $hiddenNFO = true; - } + $hiddenNfoDetection = $this->parserService->detectHiddenNfoFile($subject, $segmentCountInFile); + if ($hiddenNfoDetection !== false) { + $nfoMessageId = ['hidden' => $hiddenNfoDetection['hidden'], 'id' => $firstSegmentId, 'priority' => $hiddenNfoDetection['priority']]; + $hiddenNFO = true; } } // --- PAR2 Detection --- // Look specifically for the .par2 index file (often small, but not always 1/1) - if ($this->lookuppar2 && ! $foundPAR2 && isset($firstSegmentId) && preg_match('/\.par2$/i', $subject)) { + if ($this->lookupPar2 && ! $foundPAR2 && isset($firstSegmentId) && $this->parserService->detectPar2IndexFile($subject)) { // Attempt to parse the PAR2 file using its first segment ID if ($this->postProcessService->parsePAR2($firstSegmentId, $relID, $groupID, $this->nntp, 1) === true) { Release::query()->where('id', $relID)->update(['proc_par2' => 1]); @@ -242,20 +185,7 @@ class NZBContents } // End foreach $nzbFile->file // Calculate completion - // Avoid division by zero and handle cases where parts info might be missing/incorrect - if ($artificialParts > 0) { - $completion = min(100, ($actualParts / $artificialParts) * 100); - } elseif ($actualParts > 0) { - // If artificial parts couldn't be determined, but we have actual parts, - // we can't calculate completion accurately based on subject. - // Consider if $actualParts alone means 100% or if it's unknown. - // Setting to 100 if actual parts > 0 and artificial is 0 might be misleading. - // Let's default to 0 or another state indicating unknown completion from subject. - $completion = 0; // Or potentially set a specific status? - } else { - // If both are zero (e.g., empty NZB or parsing issue), completion is 0. - $completion = 0; - } + $completion = $this->calculateCompletion($actualParts, $artificialParts); Release::query()->where('id', $relID)->update(['completion' => $completion]); @@ -273,8 +203,6 @@ class NZBContents } // If NFO check was not requested, the function's primary goal might be just completion/PAR2 update. - // The original function returned false here. Decide if true (parsed successfully) or false is more appropriate. - // Returning false maintains original behavior when nfoCheck is false and no NFO ID is returned. return false; } @@ -282,12 +210,12 @@ class NZBContents * Loads and parses an NZB file based on a GUID. * * @param string $guid The release GUID to locate the NZB file - * @return \SimpleXMLElement|bool The parsed NZB file as SimpleXMLElement or false on failure + * @return \SimpleXMLElement|false The parsed NZB file as SimpleXMLElement or false on failure */ - public function loadNzb(string $guid): \SimpleXMLElement|bool + public function loadNzb(string $guid): \SimpleXMLElement|false { // Fetch the NZB file path using the GUID - $nzbPath = $this->nzb->NZBPath($guid); + $nzbPath = $this->nzbService->nzbPath($guid); if ($nzbPath === false) { return false; } @@ -295,7 +223,7 @@ class NZBContents // Attempt to decompress the NZB file $nzbContents = unzipGzipFile($nzbPath); if (empty($nzbContents)) { - if ($this->echooutput) { + if ($this->echoOutput) { $perms = fileperms($nzbPath); $formattedPerms = $perms !== false ? decoct($perms & 0777) : 'unknown'; echo PHP_EOL."Unable to decompress: {$nzbPath} - {$formattedPerms} - may have bad file permissions, skipping.".PHP_EOL; @@ -304,22 +232,7 @@ class NZBContents return false; } - // Safely parse the XML content - libxml_use_internal_errors(true); - $nzbFile = simplexml_load_string($nzbContents); - - if ($nzbFile === false) { - if ($this->echooutput) { - $errors = libxml_get_errors(); - $errorMsg = ! empty($errors) ? ' - XML error: '.$errors[0]->message : ''; - echo PHP_EOL."Unable to load NZB: {$guid} appears to be an invalid NZB{$errorMsg}, skipping.".PHP_EOL; - libxml_clear_errors(); - } - - return false; - } - - return $nzbFile; + return $this->parserService->parseNzbXml($nzbContents, $this->echoOutput, $guid); } /** @@ -327,7 +240,7 @@ class NZBContents * * @throws \Exception */ - public function checkPAR2(string $guid, int $relID, int $groupID, int $nameStatus, int $show): bool + public function checkPar2(string $guid, int $relID, int $groupID, int $nameStatus, int $show): bool { $nzbFile = $this->loadNzb($guid); if ($nzbFile !== false) { @@ -349,4 +262,59 @@ class NZBContents return false; } + + /** + * Calculate the completion percentage from actual and expected parts. + * + * @param int $actualParts The actual number of parts found + * @param int $artificialParts The expected number of parts from subject + * @return float The completion percentage (0-100) + */ + protected function calculateCompletion(int $actualParts, int $artificialParts): float + { + // Avoid division by zero and handle cases where parts info might be missing/incorrect + if ($artificialParts > 0) { + return min(100, ($actualParts / $artificialParts) * 100); + } elseif ($actualParts > 0) { + // If artificial parts couldn't be determined, but we have actual parts, + // we can't calculate completion accurately based on subject. + return 0; + } + + // If both are zero (e.g., empty NZB or parsing issue), completion is 0. + return 0; + } + + /** + * Set NNTP service instance. + */ + public function setNntp(NNTPService $nntp): void + { + $this->nntp = $nntp; + } + + /** + * Set NFO handler instance. + */ + public function setNfo(Nfo $nfo): void + { + $this->nfo = $nfo; + } + + /** + * Set echo output setting. + */ + public function setEchoOutput(bool $echo): void + { + $this->echoOutput = $echo; + } + + /** + * Get echo output setting. + */ + public function getEchoOutput(): bool + { + return $this->echoOutput; + } } + diff --git a/app/Services/Nzb/NzbParserService.php b/app/Services/Nzb/NzbParserService.php new file mode 100644 index 000000000..7444da0cc --- /dev/null +++ b/app/Services/Nzb/NzbParserService.php @@ -0,0 +1,310 @@ + true, + 'strip-count' => false, + ]; + $options += $defaults; + + $i = 0; + $result = []; + + if (! $nzb) { + return $result; + } + + $xml = @simplexml_load_string(str_replace("\x0F", '', $nzb)); + if (! $xml || strtolower($xml->getName()) !== 'nzb') { + return $result; + } + + foreach ($xml->file as $file) { + // Subject. + $title = (string) $file->attributes()->subject; + + if ($options['no-file-key'] === false) { + $i = $title; + if ($options['strip-count']) { + // Strip file / part count to get proper sorting. + $i = preg_replace('#\d+[- ._]?(/|\||[o0]f)[- ._]?\d+?(?![- ._]\d)#i', '', $i); + // Change .rar and .par2 to be sorted before .part0x.rar and .volxxx+xxx.par2 + if (str_contains($i, '.par2') && ! preg_match('#\.vol\d+\+\d+\.par2#i', $i)) { + $i = str_replace('.par2', '.vol0.par2', $i); + } elseif (preg_match('#\.rar[^a-z0-9]#i', $i) && ! preg_match('#\.part\d+\.rar$#i', $i)) { + $i = preg_replace('#\.rar(?:[^a-z0-9])#i', '.part0.rar', $i); + } + } + } + + $result[$i]['title'] = $title; + + // Extensions. + if (preg_match( + '/\.(\d{2,3}|7z|ace|ai7|srr|srt|sub|aiff|asc|avi|audio|bin|bz2|' + .'c|cfc|cfm|chm|class|conf|cpp|cs|css|csv|cue|deb|divx|doc|dot|' + .'eml|enc|exe|file|gif|gz|hlp|htm|html|image|iso|jar|java|jpeg|' + .'jpg|js|lua|m|m3u|mkv|mm|mov|mp3|mp4|mpg|nfo|nzb|odc|odf|odg|odi|odp|' + .'ods|odt|ogg|par2|parity|pdf|pgp|php|pl|png|ppt|ps|py|r\d{2,3}|' + .'ram|rar|rb|rm|rpm|rtf|sfv|sig|sql|srs|swf|sxc|sxd|sxi|sxw|tar|' + .'tex|tgz|txt|vcf|video|vsd|wav|wma|wmv|xls|xml|xpi|xvid|zip7|zip)' + .'[" ](?!([\)|\-]))/i', + $title, + $ext + ) + ) { + if (preg_match('/\.r\d{2,3}/i', $ext[0])) { + $ext[1] = 'rar'; + } + $result[$i]['ext'] = strtolower($ext[1]); + } else { + $result[$i]['ext'] = ''; + } + + $fileSize = $numSegments = 0; + + // Parts. + if (! isset($result[$i]['segments'])) { + $result[$i]['segments'] = []; + } + + // File size. + foreach ($file->segments->segment as $segment) { + $result[$i]['segments'][] = (string) $segment; + $fileSize += $segment->attributes()->bytes; + $numSegments++; + } + $result[$i]['size'] = $fileSize; + + // File completion. + if (preg_match('/(\d+)\)$/', $title, $parts)) { + $result[$i]['partstotal'] = $parts[1]; + } + $result[$i]['partsactual'] = $numSegments; + + // Groups. + if (! isset($result[$i]['groups'])) { + $result[$i]['groups'] = []; + } + foreach ($file->groups->group as $g) { + $result[$i]['groups'][] = (string) $g; + } + + if ($options['no-file-key']) { + $i++; + } + } + + return $result; + } + + /** + * Load and parse an NZB file from its contents string. + * + * @param string $nzbContents The decompressed NZB file contents + * @param bool $echoErrors Whether to echo errors on parse failure + * @param string $guid Optional GUID for error messages + * @return \SimpleXMLElement|false The parsed NZB file as SimpleXMLElement or false on failure + */ + public function parseNzbXml(string $nzbContents, bool $echoErrors = false, string $guid = ''): \SimpleXMLElement|false + { + if (empty($nzbContents)) { + return false; + } + + // Safely parse the XML content + libxml_use_internal_errors(true); + $nzbFile = simplexml_load_string($nzbContents); + + if ($nzbFile === false) { + if ($echoErrors) { + $errors = libxml_get_errors(); + $errorMsg = ! empty($errors) ? ' - XML error: '.$errors[0]->message : ''; + echo PHP_EOL."Unable to load NZB: {$guid} appears to be an invalid NZB{$errorMsg}, skipping.".PHP_EOL; + libxml_clear_errors(); + } + + return false; + } + + return $nzbFile; + } + + /** + * Get the file extension from a subject line. + * + * @param string $subject The file subject/name + * @return string The detected extension, or empty string if none found + */ + public function detectFileExtension(string $subject): string + { + if (preg_match( + '/\.(\d{2,3}|7z|ace|ai7|srr|srt|sub|aiff|asc|avi|audio|bin|bz2|' + .'c|cfc|cfm|chm|class|conf|cpp|cs|css|csv|cue|deb|divx|doc|dot|' + .'eml|enc|exe|file|gif|gz|hlp|htm|html|image|iso|jar|java|jpeg|' + .'jpg|js|lua|m|m3u|mkv|mm|mov|mp3|mp4|mpg|nfo|nzb|odc|odf|odg|odi|odp|' + .'ods|odt|ogg|par2|parity|pdf|pgp|php|pl|png|ppt|ps|py|r\d{2,3}|' + .'ram|rar|rb|rm|rpm|rtf|sfv|sig|sql|srs|swf|sxc|sxd|sxi|sxw|tar|' + .'tex|tgz|txt|vcf|video|vsd|wav|wma|wmv|xls|xml|xpi|xvid|zip7|zip)' + .'[" ](?!([\)|\-]))/i', + $subject, + $ext + ) + ) { + if (preg_match('/\.r\d{2,3}/i', $ext[0])) { + return 'rar'; + } + + return strtolower($ext[1]); + } + + return ''; + } + + /** + * Check if a subject indicates an NFO file. + * + * @param string $subject The file subject + * @return array|false Returns array with detection info or false if not an NFO + */ + public function detectNfoFile(string $subject): array|false + { + // Standard NFO extensions + if (preg_match('/\.\b(nfo|diz|info?)\b(?![.-])/i', $subject)) { + return ['hidden' => false, 'priority' => 1]; + } + + // Alternative NFO naming patterns (group-specific or obfuscated) + if (preg_match('/(?:^|["\s])(?:file(?:_?id)?|readme|release|info(?:rmation)?|about|desc(?:ription)?|notes?|read\.?me|00-|000-|0-|_-_).*?\.(?:txt|nfo|diz)(?:["\s]|$)/i', $subject)) { + return ['hidden' => false, 'priority' => 2]; + } + + return false; + } + + /** + * Check if a subject might indicate a hidden NFO file. + * + * @param string $subject The file subject + * @param int $segmentCount The number of segments in the file + * @return array|false Returns array with detection info or false if not a hidden NFO + */ + public function detectHiddenNfoFile(string $subject, int $segmentCount): array|false + { + $isHiddenNfoCandidate = false; + + // Pattern 1: Single segment files with (1/1) + if ($segmentCount === 1 && preg_match('/\(1\/1\)$/i', $subject)) { + $isHiddenNfoCandidate = true; + } + + // Pattern 2: Small segment count (1-2) with NFO-like names but no extension + if (! $isHiddenNfoCandidate && $segmentCount <= 2 && preg_match('/(?:^|["\s])(?:nfo|info|readme|release|file_?id|about)(?:["\s]|$)/i', $subject)) { + $isHiddenNfoCandidate = true; + } + + // Pattern 3: Scene-style NFO naming (group-release.nfo without extension visible) + if (! $isHiddenNfoCandidate && $segmentCount === 1 && preg_match('/^[a-z0-9._-]+["\s]*\(1\/1\)/i', $subject)) { + // Check for scene-like naming pattern + if (preg_match('/^[a-z0-9]+[._-][a-z0-9._-]+["\s]*\(1\/1\)/i', $subject)) { + $isHiddenNfoCandidate = true; + } + } + + // Pattern 4: Very small files (NFOs are typically small) + // Files described as very small in bytes could be NFOs + if (! $isHiddenNfoCandidate && $segmentCount === 1 && preg_match('/yEnc\s*\(\d+\)\s*\[1\/1\]/i', $subject)) { + $isHiddenNfoCandidate = true; + } + + if (! $isHiddenNfoCandidate) { + return false; + } + + // Enhanced exclusion: check if it's NOT likely another common file type + $excludedExtensions = '/\.(?:' . + // Executables + 'exe|com|bat|cmd|scr|dll|msi|pkg|deb|rpm|apk|ipa|app|' . + // Archives + 'zip|rar|[rst]\d{2}|7z|ace|tar|gz|bz2|xz|lzma|cab|iso|bin|cue|img|mdf|nrg|dmg|vhd|' . + // Audio + 'mp3|flac|ogg|aac|wav|wma|m4a|opus|ape|wv|mpc|' . + // Video + 'avi|mkv|mp4|mov|wmv|mpg|mpeg|ts|vob|m2ts|webm|flv|ogv|divx|xvid|' . + // Images + 'jpg|jpeg|png|gif|bmp|tif|tiff|psd|webp|svg|ico|raw|cr2|nef|' . + // Documents + 'pdf|doc|docx|xls|xlsx|ppt|pptx|odt|ods|odp|rtf|epub|mobi|azw|' . + // Code + 'html|htm|css|js|php|py|java|c|cpp|h|cs|sql|json|xml|yml|yaml|' . + // Data + 'db|dbf|mdb|accdb|sqlite|csv|' . + // Verification + 'par2?|sfv|md5|sha1|sha256|sha512|crc|' . + // Misc + 'url|lnk|cfg|ini|inf|sys|tmp|bak|log|srt|sub|idx|ass|ssa|vtt' . + ')\b/i'; + + if (preg_match($excludedExtensions, $subject)) { + return false; + } + + return ['hidden' => true, 'priority' => 10]; + } + + /** + * Check if a subject indicates a PAR2 index file. + * + * @param string $subject The file subject + * @return bool True if it's a PAR2 index file + */ + public function detectPar2IndexFile(string $subject): bool + { + return (bool) preg_match('/\.par2$/i', $subject); + } + + /** + * Calculate artificial parts from a subject line. + * + * @param string $subject The file subject + * @return int The estimated total parts, or 0 if not determinable + */ + public function extractPartsTotal(string $subject): int + { + // Improve artificial parts calculation robustness (e.g., "[15/20]", "(15/20)") + if (preg_match('/(?:[(\[])?(\d+)[\/)\\]](\d+)[)\]]?$/', $subject, $parts)) { + if (isset($parts[2]) && (int) $parts[2] > 0) { + return (int) $parts[2]; + } + } + + // Fallback to original simple check + if (preg_match('/(\d+)\)$/', $subject, $parts)) { + return (int) $parts[1]; + } + + return 0; + } +} + diff --git a/Blacklight/NZB.php b/app/Services/Nzb/NzbService.php old mode 100755 new mode 100644 similarity index 61% rename from Blacklight/NZB.php rename to app/Services/Nzb/NzbService.php index 725b6d531..f8d6bc65d --- a/Blacklight/NZB.php +++ b/app/Services/Nzb/NzbService.php @@ -1,6 +1,8 @@ siteNzbPath, '/')) { $this->siteNzbPath .= '/'; } - $this->_nzbCommentString = sprintf( + $this->nzbCommentString = sprintf( 'NZB Generated by: NNTmux %s', now()->format('F j, Y, g:i a O') ); - $this->_siteCommentString = sprintf( + $this->siteCommentString = sprintf( 'NZB downloaded from %s', config('app.name') ); } /** + * Write an NZB file for a release. + * * @throws \Throwable */ public function writeNzbForReleaseId(Release $release): bool @@ -93,7 +82,7 @@ class NZB return false; } - $XMLWriter = new \XMLWriter; + $XMLWriter = new \XMLWriter(); $XMLWriter->openMemory(); $XMLWriter->setIndent(true); $XMLWriter->setIndentString(' '); @@ -103,7 +92,7 @@ class NZB $XMLWriter->startDocument('1.0', 'UTF-8'); $XMLWriter->startDtd(self::NZB_DTD_NAME, self::NZB_DTD_PUBLIC, self::NZB_DTD_EXTERNAL); $XMLWriter->endDtd(); - $XMLWriter->writeComment($this->_nzbCommentString); + $XMLWriter->writeComment($this->nzbCommentString); $XMLWriter->startElement('nzb'); $XMLWriter->writeAttribute('xmlns', self::NZB_XML_NS); @@ -167,10 +156,10 @@ class NZB $XMLWriter->endElement(); // file } } - $XMLWriter->writeComment($this->_siteCommentString); + $XMLWriter->writeComment($this->siteCommentString); $XMLWriter->endElement(); // nzb $XMLWriter->endDocument(); - $path = ($this->buildNZBPath($release->guid, $this->nzbSplitLevel, true).$release->guid.'.nzb.gz'); + $path = ($this->buildNzbPath($release->guid, $this->nzbSplitLevel, true).$release->guid.'.nzb.gz'); $fp = gzopen($path, 'wb7'); if (! $fp) { return false; @@ -206,7 +195,7 @@ class NZB * @param bool $createIfNotExist Create the folder if it doesn't exist. * @return string $nzbpath The path to store the NZB file. */ - public function buildNZBPath(string $releaseGuid, int $levelsToSplit, bool $createIfNotExist): string + public function buildNzbPath(string $releaseGuid, int $levelsToSplit, bool $createIfNotExist): string { $nzbPath = ''; @@ -231,132 +220,77 @@ class NZB * @param bool $createIfNotExist Create the folder if it doesn't exist. (optional) * @return string Path+filename. */ - public function getNZBPath(string $releaseGuid, int $levelsToSplit = 0, bool $createIfNotExist = false): string + public function getNzbPath(string $releaseGuid, int $levelsToSplit = 0, bool $createIfNotExist = false): string { if ($levelsToSplit === 0) { $levelsToSplit = $this->nzbSplitLevel; } - return $this->buildNZBPath($releaseGuid, $levelsToSplit, $createIfNotExist).$releaseGuid.'.nzb.gz'; + return $this->buildNzbPath($releaseGuid, $levelsToSplit, $createIfNotExist).$releaseGuid.'.nzb.gz'; } /** - * Determine is an NZB exists, returning the path+filename, if not return false. + * Determine if an NZB exists, returning the path+filename, if not return false. * * @param string $releaseGuid The guid of the release. * @return false|string On success: (string) Path+file name of the nzb. - * On failure: false . + * On failure: false. */ - public function NZBPath(string $releaseGuid): bool|string + public function nzbPath(string $releaseGuid): bool|string { - $nzbFile = $this->getNZBPath($releaseGuid); + $nzbFile = $this->getNzbPath($releaseGuid); return File::isFile($nzbFile) ? $nzbFile : false; } /** - * Retrieve various information on a NZB file (the subject, # of pars, - * file extensions, file sizes, file completion, group names, # of parts). + * Read and decompress an NZB file contents. * - * @param string $nzb The NZB contents in a string. - * @return array $result Empty if not an NZB or the contents of the NZB. + * @param string $releaseGuid The release GUID + * @return string|false The decompressed NZB contents or false on failure */ - public function nzbFileList(string $nzb, array $options = []): array + public function readNzbContents(string $releaseGuid): string|false { - $defaults = [ - 'no-file-key' => true, - 'strip-count' => false, - ]; - $options += $defaults; - - $i = 0; - $result = []; - - if (! $nzb) { - return $result; + $nzbPath = $this->nzbPath($releaseGuid); + if ($nzbPath === false) { + return false; } - $xml = @simplexml_load_string(str_replace("\x0F", '', $nzb)); - if (! $xml || strtolower($xml->getName()) !== 'nzb') { - return $result; + $contents = unzipGzipFile($nzbPath); + + return ! empty($contents) ? $contents : false; + } + + /** + * Delete an NZB file. + * + * @param string $releaseGuid The release GUID + * @return bool True if deleted, false otherwise + */ + public function deleteNzb(string $releaseGuid): bool + { + $nzbPath = $this->nzbPath($releaseGuid); + if ($nzbPath === false) { + return false; } - foreach ($xml->file as $file) { - // Subject. - $title = (string) $file->attributes()->subject; + return File::delete($nzbPath); + } - if ($options['no-file-key'] === false) { - $i = $title; - if ($options['strip-count']) { - // Strip file / part count to get proper sorting. - $i = preg_replace('#\d+[- ._]?(/|\||[o0]f)[- ._]?\d+?(?![- ._]\d)#i', '', $i); - // Change .rar and .par2 to be sorted before .part0x.rar and .volxxx+xxx.par2 - if (str_contains($i, '.par2') && ! preg_match('#\.vol\d+\+\d+\.par2#i', $i)) { - $i = str_replace('.par2', '.vol0.par2', $i); - } elseif (preg_match('#\.rar[^a-z0-9]#i', $i) && ! preg_match('#\.part\d+\.rar$#i', $i)) { - $i = preg_replace('#\.rar(?:[^a-z0-9])#i', '.part0.rar', $i); - } - } - } + /** + * Get the default NZB split level. + */ + public function getNzbSplitLevel(): int + { + return $this->nzbSplitLevel; + } - $result[$i]['title'] = $title; - - // Extensions. - if (preg_match( - '/\.(\d{2,3}|7z|ace|ai7|srr|srt|sub|aiff|asc|avi|audio|bin|bz2|' - .'c|cfc|cfm|chm|class|conf|cpp|cs|css|csv|cue|deb|divx|doc|dot|' - .'eml|enc|exe|file|gif|gz|hlp|htm|html|image|iso|jar|java|jpeg|' - .'jpg|js|lua|m|m3u|mkv|mm|mov|mp3|mp4|mpg|nfo|nzb|odc|odf|odg|odi|odp|' - .'ods|odt|ogg|par2|parity|pdf|pgp|php|pl|png|ppt|ps|py|r\d{2,3}|' - .'ram|rar|rb|rm|rpm|rtf|sfv|sig|sql|srs|swf|sxc|sxd|sxi|sxw|tar|' - .'tex|tgz|txt|vcf|video|vsd|wav|wma|wmv|xls|xml|xpi|xvid|zip7|zip)' - .'[" ](?!([\)|\-]))/i', - $title, - $ext - ) - ) { - if (preg_match('/\.r\d{2,3}/i', $ext[0])) { - $ext[1] = 'rar'; - } - $result[$i]['ext'] = strtolower($ext[1]); - } else { - $result[$i]['ext'] = ''; - } - - $fileSize = $numSegments = 0; - - // Parts. - if (! isset($result[$i]['segments'])) { - $result[$i]['segments'] = []; - } - - // File size. - foreach ($file->segments->segment as $segment) { - $result[$i]['segments'][] = (string) $segment; - $fileSize += $segment->attributes()->bytes; - $numSegments++; - } - $result[$i]['size'] = $fileSize; - - // File completion. - if (preg_match('/(\d+)\)$/', $title, $parts)) { - $result[$i]['partstotal'] = $parts[1]; - } - $result[$i]['partsactual'] = $numSegments; - - // Groups. - if (! isset($result[$i]['groups'])) { - $result[$i]['groups'] = []; - } - foreach ($file->groups->group as $g) { - $result[$i]['groups'][] = (string) $g; - } - - if ($options['no-file-key']) { - $i++; - } - } - - return $result; + /** + * Get the base NZB storage path. + */ + public function getSiteNzbPath(): string + { + return $this->siteNzbPath; } } + diff --git a/app/Services/ReleaseCreationService.php b/app/Services/ReleaseCreationService.php index 9ba1388ee..13e9a8558 100644 --- a/app/Services/ReleaseCreationService.php +++ b/app/Services/ReleaseCreationService.php @@ -11,9 +11,9 @@ use App\Models\ReleaseRegex; use App\Models\ReleasesGroups; use App\Models\UsenetGroup; use App\Services\Categorization\CategorizationService; +use App\Services\Nzb\NzbService; use App\Services\ReleaseCleaningService; use Blacklight\ColorCLI; -use Blacklight\NZB; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -113,7 +113,7 @@ class ReleaseCreationService 'categories_id' => $determinedCategory['categories_id'] ?? Category::OTHER_MISC, 'isrenamed' => $properName === true ? 1 : 0, 'predb_id' => $preID === false ? 0 : $preID, - 'nzbstatus' => NZB::NZB_NONE, + 'nzbstatus' => NzbService::NZB_NONE, 'ishashed' => preg_match('/^[a-fA-F0-9]{32}\b|^[a-fA-F0-9]{40}\b|^[a-fA-F0-9]{64}\b|^[a-fA-F0-9]{96}\b|^[a-fA-F0-9]{128}\b/i', $searchName) ? 1 : 0, ]); diff --git a/app/Services/ReleaseProcessingService.php b/app/Services/ReleaseProcessingService.php index 9002c787d..bfa0ddf74 100644 --- a/app/Services/ReleaseProcessingService.php +++ b/app/Services/ReleaseProcessingService.php @@ -13,6 +13,7 @@ use App\Models\Release; use App\Models\Settings; use App\Models\UsenetGroup; use App\Services\Categorization\CategorizationService; +use App\Services\Nzb\NzbService; use App\Services\Releases\ReleaseManagementService; use App\Support\DTOs\ProcessReleasesSettings; use App\Support\DTOs\ReleaseCreationResult; @@ -20,7 +21,6 @@ use App\Support\DTOs\ReleaseDeleteStats; use App\Services\NNTP\NNTPService; use Blacklight\ColorCLI; use Blacklight\Genres; -use Blacklight\NZB; use DateTimeInterface; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; @@ -49,7 +49,7 @@ final class ReleaseProcessingService private bool $echoCLI; private readonly ProcessReleasesSettings $settings; private readonly ColorCLI $colorCLI; - private readonly NZB $nzb; + private readonly NzbService $nzb; private readonly ReleaseCleaningService $releaseCleaning; private readonly ReleaseManagementService $releaseManagement; private readonly ReleaseImageService $releaseImage; @@ -59,7 +59,7 @@ final class ReleaseProcessingService public function __construct( ?ColorCLI $colorCLI = null, - ?NZB $nzb = null, + ?NzbService $nzb = null, ?ReleaseCleaningService $releaseCleaning = null, ?ReleaseManagementService $releaseManagement = null, ?ReleaseImageService $releaseImage = null, @@ -70,7 +70,7 @@ final class ReleaseProcessingService $this->echoCLI = (bool) config('nntmux.echocli'); $this->colorCLI = $colorCLI ?? new ColorCLI(); - $this->nzb = $nzb ?? new NZB(); + $this->nzb = $nzb ?? app(NzbService::class); $this->releaseCleaning = $releaseCleaning ?? new ReleaseCleaningService(); $this->releaseManagement = $releaseManagement ?? app(ReleaseManagementService::class); $this->releaseImage = $releaseImage ?? new ReleaseImageService(); @@ -482,7 +482,7 @@ final class ReleaseProcessingService $query = Release::query() ->with('category.parent') - ->where('nzbstatus', '=', NZB::NZB_NONE) + ->where('nzbstatus', '=', NzbService::NZB_NONE) ->select(['id', 'guid', 'name', 'categories_id']); if (!empty($groupID)) { diff --git a/app/Services/ReleaseRemoverService.php b/app/Services/ReleaseRemoverService.php index f110154bc..60ae4bf97 100644 --- a/app/Services/ReleaseRemoverService.php +++ b/app/Services/ReleaseRemoverService.php @@ -7,9 +7,9 @@ namespace App\Services; use App\Enums\BlacklistConstants; use App\Models\Category; use App\Models\Settings; +use App\Services\Nzb\NzbService; use App\Services\Releases\ReleaseManagementService; use Blacklight\ColorCLI; -use Blacklight\NZB; use Exception; use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; @@ -48,7 +48,7 @@ class ReleaseRemoverService protected string $query = ''; protected ReleaseManagementService $releaseManagement; protected array $result = []; - private NZB $nzb; + private NzbService $nzb; private ReleaseImageService $releaseImage; /** @@ -59,12 +59,12 @@ class ReleaseRemoverService public function __construct( ?ColorCLI $colorCLI = null, ?ReleaseManagementService $releaseManagement = null, - ?NZB $nzb = null, + ?NzbService $nzb = null, ?ReleaseImageService $releaseImage = null ) { $this->colorCLI = $colorCLI ?? new ColorCLI; $this->releaseManagement = $releaseManagement ?? app(ReleaseManagementService::class); - $this->nzb = $nzb ?? new NZB; + $this->nzb = $nzb ?? app(NzbService::class); $this->releaseImage = $releaseImage ?? new ReleaseImageService; $this->echoCLI = config('nntmux.echocli'); diff --git a/app/Services/Releases/ReleaseManagementService.php b/app/Services/Releases/ReleaseManagementService.php index 0c472fbb3..e73a2e12a 100644 --- a/app/Services/Releases/ReleaseManagementService.php +++ b/app/Services/Releases/ReleaseManagementService.php @@ -4,8 +4,8 @@ namespace App\Services\Releases; use App\Facades\Search; use App\Models\Release; +use App\Services\Nzb\NzbService; use App\Services\ReleaseImageService; -use Blacklight\NZB; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; @@ -25,7 +25,7 @@ class ReleaseManagementService { $list = (array) $list; - $nzb = new NZB; + $nzb = app(NzbService::class); $releaseImage = new ReleaseImageService; foreach ($list as $identifier) { @@ -41,10 +41,10 @@ class ReleaseManagementService * * @throws \Exception */ - public function deleteSingle(array $identifiers, NZB $nzb, ReleaseImageService $releaseImage): void + public function deleteSingle(array $identifiers, NzbService $nzb, ReleaseImageService $releaseImage): void { // Delete NZB from disk. - $nzbPath = $nzb->NZBPath($identifiers['g']); + $nzbPath = $nzb->nzbPath($identifiers['g']); if (! empty($nzbPath)) { File::delete($nzbPath); } @@ -76,7 +76,7 @@ class ReleaseManagementService * * @throws \Exception */ - public function deleteSingleWithService(array $identifiers, NZB $nzb, ReleaseImageService $releaseImage): void + public function deleteSingleWithService(array $identifiers, NzbService $nzb, ReleaseImageService $releaseImage): void { $this->deleteSingle($identifiers, $nzb, $releaseImage); } diff --git a/app/Services/Releases/ReleaseSearchService.php b/app/Services/Releases/ReleaseSearchService.php index 7a3265037..c304fa7da 100644 --- a/app/Services/Releases/ReleaseSearchService.php +++ b/app/Services/Releases/ReleaseSearchService.php @@ -326,37 +326,23 @@ class ReleaseSearchService } if (! $hasValidSiteIds) { + // Build search name with season/episode for the full-text search if (! empty($series) && (int) $series < 1900) { $searchName .= sprintf(' S%s', str_pad($series, 2, '0', STR_PAD_LEFT)); $seriesNum = (int) preg_replace('/^s0*/i', '', $series); - $conditions[] = sprintf('tve.series = %d', $seriesNum); - $needsEpisodeJoin = true; if (! empty($episode) && ! str_contains($episode, '/')) { $searchName .= sprintf('E%s', str_pad($episode, 2, '0', STR_PAD_LEFT)); $episodeNum = (int) preg_replace('/^e0*/i', '', $episode); - $conditions[] = sprintf('tve.episode = %d', $episodeNum); } } elseif (! empty($airDate)) { $searchName .= ' '.str_replace(['/', '-', '.', '_'], ' ', $airDate); - $conditions[] = sprintf('DATE(tve.firstaired) = %s', escapeString($airDate)); - $needsEpisodeJoin = true; } } - // Try Elasticsearch first if enabled - if (config('nntmux.elasticsearch_enabled') === true) { - $searchResult = $this->elasticSearch->indexSearchTMA($searchName, $limit); - } + // Use the unified Search facade + $searchResult = Search::searchReleases(['searchname' => $searchName], $limit); - // Fall back to Manticore if Elasticsearch didn't return results - if (empty($searchResult)) { - $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $searchName, ['searchname']); - if (! empty($searchResult)) { - $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); - } - } - - // Fall back to MySQL if both Elasticsearch and Manticore failed (only if enabled) + // Fall back to MySQL if search engine failed (only if enabled) if (empty($searchResult) && config('nntmux.mysql_search_fallback', false) === true) { $searchResult = $this->performMySQLSearch(['searchname' => $searchName], $limit); } @@ -366,6 +352,43 @@ class ReleaseSearchService } $conditions[] = sprintf('r.id IN (%s)', implode(',', array_map('intval', $searchResult))); + + // Try to add episode conditions if season/episode data is provided and no valid site IDs + // This will filter results to only those with matching episode data in tv_episodes table + // If this results in no matches, we'll fall back to results without episode conditions + if (! $hasValidSiteIds && (! empty($series) || ! empty($airDate))) { + $episodeConditions = []; + if (! empty($series) && (int) $series < 1900) { + $seriesNum = (int) preg_replace('/^s0*/i', '', $series); + $episodeConditions[] = sprintf('tve.series = %d', $seriesNum); + if (! empty($episode) && ! str_contains($episode, '/')) { + $episodeNum = (int) preg_replace('/^e0*/i', '', $episode); + $episodeConditions[] = sprintf('tve.episode = %d', $episodeNum); + } + } elseif (! empty($airDate)) { + $episodeConditions[] = sprintf('DATE(tve.firstaired) = %s', escapeString($airDate)); + } + + if (! empty($episodeConditions)) { + // Check if any of the found releases have matching episode data + $checkSql = sprintf( + 'SELECT r.id FROM releases r INNER JOIN tv_episodes tve ON r.tv_episodes_id = tve.id WHERE r.id IN (%s) AND %s LIMIT 1', + implode(',', array_map('intval', $searchResult)), + implode(' AND ', $episodeConditions) + ); + $hasEpisodeMatches = Release::fromQuery($checkSql); + + if ($hasEpisodeMatches->isNotEmpty()) { + // Some releases have matching episode data, add the conditions + foreach ($episodeConditions as $cond) { + $conditions[] = $cond; + } + $needsEpisodeJoin = true; + } + // If no matches with episode data, don't add episode conditions + // The search will return results based on searchname match only + } + } } $catQuery = Category::getCategorySearch($cat, 'tv'); @@ -492,20 +515,10 @@ class ReleaseSearchService } $searchResult = []; if (! empty($name)) { - // Try Elasticsearch first if enabled - if (config('nntmux.elasticsearch_enabled') === true) { - $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit); - } + // Use the unified Search facade + $searchResult = Search::searchReleases(['searchname' => $name], $limit); - // Fall back to Manticore if Elasticsearch didn't return results - if (empty($searchResult)) { - $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']); - if (! empty($searchResult)) { - $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); - } - } - - // Fall back to MySQL if both Elasticsearch and Manticore failed (only if enabled) + // Fall back to MySQL if search engine failed (only if enabled) if (empty($searchResult) && config('nntmux.mysql_search_fallback', false) === true) { $searchResult = $this->performMySQLSearch(['searchname' => $name], $limit); } diff --git a/misc/testing/Dev/clean_nzbs.php b/misc/testing/Dev/clean_nzbs.php index f94f1f627..6eec9b333 100644 --- a/misc/testing/Dev/clean_nzbs.php +++ b/misc/testing/Dev/clean_nzbs.php @@ -3,9 +3,10 @@ require_once dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'bootstrap/autoload.php'; use App\Models\Release; +use App\Services\Nzb\NzbParserService; +use App\Services\Nzb\NzbService; use App\Services\ReleaseImageService; use Blacklight\ColorCLI; -use Blacklight\NZB; use Blacklight\Releases; use Illuminate\Support\Facades\File; @@ -24,7 +25,8 @@ if (! File::isDirectory($dir) && ! File::makeDirectory($dir)) { } $releases = new Releases; -$nzb = new NZB; +$nzb = app(NzbService::class); +$nzbParser = app(NzbParserService::class); $releaseImage = new ReleaseImageService; $timestart = now()->toRfc2822String(); @@ -41,7 +43,7 @@ foreach ($itr as $filePath) { $guid = stristr($filePath->getFilename(), '.nzb.gz', true); if (File::isFile($filePath) && $guid) { $nzbfile = unzipGzipFile($filePath); - $nzbContents = $nzb->nzbFileList($nzbfile, ['no-file-key' => false, 'strip-count' => true]); + $nzbContents = $nzbParser->parseNzbFileList($nzbfile, ['no-file-key' => false, 'strip-count' => true]); if (! $nzbfile || ! @simplexml_load_string($nzbfile) || count($nzbContents) === 0) { if ($argv[1] === 'move') { rename($filePath, $dir.$guid.'.nzb.gz'); @@ -62,7 +64,7 @@ $checked = $deleted = 0; $res = Release::query()->select(['id', 'guid', 'nzbstatus'])->get(); foreach ($res as $row) { - $nzbpath = $nzb->getNZBPath($row->guid); + $nzbpath = $nzb->getNzbPath($row->guid); if (! File::isFile($nzbpath)) { $deleted++; $releases->deleteSingle(['g' => $row->guid, 'i' => $row->id], $nzb, $releaseImage); diff --git a/misc/testing/NZB/nzb-reorg.php b/misc/testing/NZB/nzb-reorg.php index ef5ceb579..31f607465 100755 --- a/misc/testing/NZB/nzb-reorg.php +++ b/misc/testing/NZB/nzb-reorg.php @@ -3,14 +3,14 @@ require_once dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'bootstrap/autoload.php'; use App\Models\Settings; +use App\Services\Nzb\NzbService; use Blacklight\ColorCLI; -use Blacklight\NZB; if (! isset($argv[1]) || ! isset($argv[2])) { exit("ERROR: You must supply the level you want to reorganize it to, and the source directory (You would use: 3 .../newznab/resources/nzb/ to move it to 3 levels deep)\n"); } -$nzb = new NZB; +$nzb = app(NzbService::class); $consoleTools = new ColorCLI; $newLevel = $argv[1]; @@ -28,7 +28,7 @@ foreach ($objects as $filestoprocess => $nzbFile) { continue; } - $newFileName = $nzb->getNZBPath( + $newFileName = $nzb->getNzbPath( str_replace('.nzb.gz', '', $nzbFile->getBasename()), $newLevel, true diff --git a/misc/testing/PostProc/check_previews.php b/misc/testing/PostProc/check_previews.php index 159c3d9ee..dbe0c30f8 100644 --- a/misc/testing/PostProc/check_previews.php +++ b/misc/testing/PostProc/check_previews.php @@ -6,9 +6,9 @@ require_once dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'bootstrap/autoload.php'; use App\Models\Release; +use App\Services\Nzb\NzbService; use App\Services\ReleaseImageService; use Blacklight\ColorCLI; -use Blacklight\NZB; use Blacklight\Releases; use Illuminate\Support\Facades\DB; @@ -19,7 +19,7 @@ $path2preview = storage_path('covers/preview'); if (isset($argv[1]) && ($argv[1] === 'true' || $argv[1] === 'check')) { $releases = new Releases; - $nzb = new NZB; + $nzb = app(NzbService::class); $releaseImage = new ReleaseImageService; $consoletools = new ColorCLI; $couldbe = $argv[1] === 'true' ? $couldbe = 'were ' : 'could be ';