mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 00:01:21 +00:00
Fix search and create nzb services
This commit is contained in:
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
+10
-12
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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!');
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
|
||||
Executable → Regular
+110
-142
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Nzb;
|
||||
|
||||
/**
|
||||
* Service for parsing and extracting information from NZB file contents.
|
||||
* Handles parsing NZB XML structure and extracting file lists, extensions, sizes, etc.
|
||||
*/
|
||||
class NzbParserService
|
||||
{
|
||||
/**
|
||||
* Retrieve various information on an NZB file (the subject, # of pars,
|
||||
* file extensions, file sizes, file completion, group names, # of parts).
|
||||
*
|
||||
* @param string $nzb The NZB contents in a string.
|
||||
* @param array $options Optional settings:
|
||||
* - 'no-file-key': Use numeric keys instead of subject (default: true)
|
||||
* - 'strip-count': Strip file/part count from subject for sorting (default: false)
|
||||
* @return array $result Empty if not an NZB or the contents of the NZB.
|
||||
*/
|
||||
public function parseNzbFileList(string $nzb, array $options = []): array
|
||||
{
|
||||
$defaults = [
|
||||
'no-file-key' => 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;
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
+62
-128
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Blacklight;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Nzb;
|
||||
|
||||
use App\Models\Binary;
|
||||
use App\Models\Collection;
|
||||
@@ -12,13 +14,10 @@ use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class for reading and writing NZB files on the hard disk,
|
||||
* building folder paths to store the NZB files.
|
||||
*
|
||||
*
|
||||
* Class NZB
|
||||
* Service for managing NZB files on disk.
|
||||
* Handles reading, writing, and path management for NZB files.
|
||||
*/
|
||||
class NZB
|
||||
class NzbService
|
||||
{
|
||||
public const NZB_NONE = 0; // Release has no NZB file yet.
|
||||
|
||||
@@ -42,25 +41,13 @@ class NZB
|
||||
*/
|
||||
protected string $siteNzbPath;
|
||||
|
||||
/**
|
||||
* Group id when writing NZBs.
|
||||
*/
|
||||
protected int $groupID;
|
||||
|
||||
public \PDO $pdo;
|
||||
|
||||
protected bool $_debug = false;
|
||||
|
||||
/**
|
||||
* String used for head in NZB XML file.
|
||||
*/
|
||||
protected string $_nzbCommentString;
|
||||
protected string $nzbCommentString;
|
||||
|
||||
protected string $_siteCommentString;
|
||||
protected string $siteCommentString;
|
||||
|
||||
/**
|
||||
* NZB constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$nzbSplitLevel = (int) Settings::settingValue('nzbsplitlevel');
|
||||
@@ -69,17 +56,19 @@ class NZB
|
||||
if (! Str::endsWith($this->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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ';
|
||||
|
||||
Reference in New Issue
Block a user