NzbContentsService::getNfoFromNzb()   B
last analyzed

Complexity

Conditions 10
Paths 8

Size

Total Lines 48
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 20
c 1
b 0
f 0
dl 0
loc 48
rs 7.6666
cc 10
nc 8
nop 4

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Services\Nzb;
6
7
use App\Models\Release;
8
use App\Models\Settings;
9
use App\Services\NNTP\NNTPService;
10
use App\Services\PostProcessService;
11
use Blacklight\Nfo;
12
13
/**
14
 * Service for processing NZB contents - extracting NFO files, PAR2 information,
15
 * and calculating release completion from NZB files.
16
 */
17
class NzbContentsService
18
{
19
    protected NzbService $nzbService;
20
21
    protected NzbParserService $parserService;
22
23
    protected NNTPService $nntp;
24
25
    protected Nfo $nfo;
26
27
    protected PostProcessService $postProcessService;
28
29
    protected bool $lookupPar2;
30
31
    protected bool $echoOutput;
32
33
    protected bool $alternateNntp;
34
35
    public function __construct(
36
        ?NzbService $nzbService = null,
37
        ?NzbParserService $parserService = null,
38
        ?NNTPService $nntp = null,
39
        ?Nfo $nfo = null,
40
        ?PostProcessService $postProcessService = null
41
    ) {
42
        $this->echoOutput = (bool) config('nntmux.echocli');
43
        $this->nzbService = $nzbService ?? app(NzbService::class);
44
        $this->parserService = $parserService ?? app(NzbParserService::class);
45
        $this->nntp = $nntp ?? new NNTPService();
46
        $this->nfo = $nfo ?? new Nfo();
47
        $this->postProcessService = $postProcessService ?? app(PostProcessService::class);
48
        $this->lookupPar2 = (int) Settings::settingValue('lookuppar2') === 1;
49
        $this->alternateNntp = (bool) config('nntmux_nntp.use_alternate_nntp_server');
50
    }
51
52
    /**
53
     * Look for an .nfo file in the NZB, download it, verify it, and return the content.
54
     *
55
     * @param  string  $guid  The release GUID.
56
     * @param  int  $relID  The release ID.
57
     * @param  int  $groupID  The group ID.
58
     * @param  string  $groupName  The group name.
59
     * @return string|false The verified NFO content as a string, or false if not found, download failed, or verification failed.
60
     *
61
     * @throws \Exception If NNTP operations fail.
62
     */
63
    public function getNfoFromNzb(string $guid, int $relID, int $groupID, string $groupName): string|false
64
    {
65
        // Step 1: Attempt to find a potential NFO message ID
66
        $messageID = $this->parseNzb($guid, $relID, $groupID, true);
67
68
        // If no NFO message ID found
69
        if ($messageID === false || ! isset($messageID['id'])) {
70
            if ($this->echoOutput) {
71
                echo '-';
72
            }
73
            // Make sure we set status to NFO_NONFO
74
            Release::query()->where('id', $relID)->update(['nfostatus' => Nfo::NFO_NONFO]);
75
76
            return false;
77
        }
78
79
        // Step 2: Attempt to download the potential NFO content
80
        $fetchedBinary = $this->nntp->getMessages($groupName, $messageID['id'], $this->alternateNntp);
81
82
        // Check if download failed
83
        if ($this->nntp->isError($fetchedBinary)) {
84
            // NFO download failed, decrement attempts to allow retries
85
            Release::query()->where('id', $relID)->decrement('nfostatus');
86
            if ($this->echoOutput) {
87
                echo 'f';
88
            }
89
90
            return false;
91
        }
92
93
        // Step 3: Verify if the downloaded content is actually an NFO file
94
        if ($this->nfo->isNFO($fetchedBinary, $guid)) {
95
            // NFO verification successful
96
            if ($this->echoOutput) {
97
                // Show if it was found via explicit name (+) or potentially hidden (*)
98
                echo $messageID['hidden'] === false ? '+' : '*';
99
            }
100
101
            return $fetchedBinary;
102
        }
103
104
        // Step 4: Handle verification failure - not a valid NFO
105
        if ($this->echoOutput) {
106
            echo '-';
107
        }
108
        Release::query()->where('id', $relID)->update(['nfostatus' => Nfo::NFO_NONFO]);
109
110
        return false;
111
    }
112
113
    /**
114
     * Gets the completion from the NZB, optionally looks if there is an NFO/PAR2 file.
115
     *
116
     * @param  string  $guid  The release GUID.
117
     * @param  int  $relID  The release ID.
118
     * @param  int  $groupID  The group ID.
119
     * @param  bool  $nfoCheck  Whether to specifically look for an NFO file.
120
     * @return array|false An array containing NFO message ID and hidden status, or false if not found/error.
121
     *
122
     * @throws \Exception If NNTP operations fail.
123
     */
124
    public function parseNzb(string $guid, int $relID, int $groupID, bool $nfoCheck = false): bool|array
125
    {
126
        $nzbFile = $this->loadNzb($guid);
127
        if ($nzbFile === false) {
128
            return false;
129
        }
130
131
        $messageID = $hiddenID = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $messageID is dead and can be removed.
Loading history...
Unused Code introduced by
The assignment to $hiddenID is dead and can be removed.
Loading history...
132
        $actualParts = $artificialParts = 0;
133
        // Initialize foundPAR2 based on settings; if lookupPar2 is false, we don't need to find one.
134
        $foundPAR2 = $this->lookupPar2 === false;
135
        // Initialize NFO flags based on whether we are checking for NFOs.
136
        $foundNFO = $hiddenNFO = $nfoCheck === false;
137
        $nfoMessageId = null; // Store potential NFO message ID here
138
139
        foreach ($nzbFile->file as $nzbContents) {
140
            $segmentCountInFile = 0;
141
            $firstSegmentId = null; // Initialize here for each file
142
            foreach ($nzbContents->segments->segment as $segment) {
143
                $actualParts++;
144
                $segmentCountInFile++;
145
                // Store the first segment ID of the current file, potentially useful for NFO/PAR2
146
                if ($segmentCountInFile === 1) {
147
                    $firstSegmentId = (string) $segment;
148
                }
149
            }
150
151
            $subject = (string) $nzbContents->attributes()->subject;
0 ignored issues
show
Bug introduced by
The method attributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

151
            $subject = (string) $nzbContents->/** @scrutinizer ignore-call */ attributes()->subject;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
152
153
            // Calculate artificial parts from subject
154
            $artificialParts += $this->parserService->extractPartsTotal($subject);
155
156
            // --- NFO Detection ---
157
            // Check for explicit NFO files first (with enhanced patterns)
158
            if ($nfoCheck && ! $foundNFO && isset($firstSegmentId)) {
159
                $nfoDetection = $this->parserService->detectNfoFile($subject);
160
                if ($nfoDetection !== false) {
161
                    $nfoMessageId = ['hidden' => $nfoDetection['hidden'], 'id' => $firstSegmentId, 'priority' => $nfoDetection['priority']];
162
                    $foundNFO = true;
163
                }
164
            }
165
166
            // Check for potential "hidden" NFOs with improved detection
167
            // Only consider this if an explicit NFO wasn't found yet
168
            if ($nfoCheck && ! $foundNFO && ! $hiddenNFO && isset($firstSegmentId)) {
169
                $hiddenNfoDetection = $this->parserService->detectHiddenNfoFile($subject, $segmentCountInFile);
170
                if ($hiddenNfoDetection !== false) {
171
                    $nfoMessageId = ['hidden' => $hiddenNfoDetection['hidden'], 'id' => $firstSegmentId, 'priority' => $hiddenNfoDetection['priority']];
172
                    $hiddenNFO = true;
173
                }
174
            }
175
176
            // --- PAR2 Detection ---
177
            // Look specifically for the .par2 index file (often small, but not always 1/1)
178
            if ($this->lookupPar2 && ! $foundPAR2 && isset($firstSegmentId) && $this->parserService->detectPar2IndexFile($subject)) {
179
                // Attempt to parse the PAR2 file using its first segment ID
180
                if ($this->postProcessService->parsePAR2($firstSegmentId, $relID, $groupID, $this->nntp, 1) === true) {
181
                    Release::query()->where('id', $relID)->update(['proc_par2' => 1]);
182
                    $foundPAR2 = true;
183
                }
184
            }
185
        } // End foreach $nzbFile->file
186
187
        // Calculate completion
188
        $completion = $this->calculateCompletion($actualParts, $artificialParts);
189
190
        Release::query()->where('id', $relID)->update(['completion' => $completion]);
191
192
        // If NFO check was requested, return the found message ID (prioritizing explicit)
193
        if ($nfoCheck && $nfoMessageId !== null && isset($nfoMessageId['id']) && \strlen($nfoMessageId['id']) > 1) {
194
            return $nfoMessageId;
195
        }
196
197
        // If NFO check was requested but nothing suitable was found
198
        if ($nfoCheck && $nfoMessageId === null) {
199
            // Update status to indicate no NFO was found in the NZB structure
200
            Release::query()->where('id', $relID)->update(['nfostatus' => Nfo::NFO_NONFO]);
201
202
            return false;
203
        }
204
205
        // If NFO check was not requested, the function's primary goal might be just completion/PAR2 update.
206
        return false;
207
    }
208
209
    /**
210
     * Loads and parses an NZB file based on a GUID.
211
     *
212
     * @param  string  $guid  The release GUID to locate the NZB file
213
     * @return \SimpleXMLElement|false The parsed NZB file as SimpleXMLElement or false on failure
214
     */
215
    public function loadNzb(string $guid): \SimpleXMLElement|false
216
    {
217
        // Fetch the NZB file path using the GUID
218
        $nzbPath = $this->nzbService->nzbPath($guid);
219
        if ($nzbPath === false) {
220
            return false;
221
        }
222
223
        // Attempt to decompress the NZB file
224
        $nzbContents = unzipGzipFile($nzbPath);
225
        if (empty($nzbContents)) {
226
            if ($this->echoOutput) {
227
                $perms = fileperms($nzbPath);
228
                $formattedPerms = $perms !== false ? decoct($perms & 0777) : 'unknown';
229
                echo PHP_EOL."Unable to decompress: {$nzbPath} - {$formattedPerms} - may have bad file permissions, skipping.".PHP_EOL;
230
            }
231
232
            return false;
233
        }
234
235
        return $this->parserService->parseNzbXml($nzbContents, $this->echoOutput, $guid);
236
    }
237
238
    /**
239
     * Attempts to get the release name from a par2 file.
240
     *
241
     * @throws \Exception
242
     */
243
    public function checkPar2(string $guid, int $relID, int $groupID, int $nameStatus, int $show): bool
244
    {
245
        $nzbFile = $this->loadNzb($guid);
246
        if ($nzbFile !== false) {
247
            foreach ($nzbFile->file as $nzbContents) {
248
                if ($nameStatus === 1
249
                    && $this->postProcessService->parsePAR2((string) $nzbContents->segments->segment, $relID, $groupID, $this->nntp, $show)
250
                    && preg_match('/\.(par[2" ]|\d{2,3}").+\(1\/1\)/i', (string) $nzbContents->attributes()->subject)
251
                ) {
252
                    Release::query()->where('id', $relID)->update(['proc_par2' => 1]);
253
254
                    return true;
255
                }
256
            }
257
        }
258
259
        if ($nameStatus === 1) {
260
            Release::query()->where('id', $relID)->update(['proc_par2' => 1]);
261
        }
262
263
        return false;
264
    }
265
266
    /**
267
     * Calculate the completion percentage from actual and expected parts.
268
     *
269
     * @param  int  $actualParts  The actual number of parts found
270
     * @param  int  $artificialParts  The expected number of parts from subject
271
     * @return float The completion percentage (0-100)
272
     */
273
    protected function calculateCompletion(int $actualParts, int $artificialParts): float
274
    {
275
        // Avoid division by zero and handle cases where parts info might be missing/incorrect
276
        if ($artificialParts > 0) {
277
            return min(100, ($actualParts / $artificialParts) * 100);
278
        } elseif ($actualParts > 0) {
279
            // If artificial parts couldn't be determined, but we have actual parts,
280
            // we can't calculate completion accurately based on subject.
281
            return 0;
282
        }
283
284
        // If both are zero (e.g., empty NZB or parsing issue), completion is 0.
285
        return 0;
286
    }
287
288
    /**
289
     * Set NNTP service instance.
290
     */
291
    public function setNntp(NNTPService $nntp): void
292
    {
293
        $this->nntp = $nntp;
294
    }
295
296
    /**
297
     * Set NFO handler instance.
298
     */
299
    public function setNfo(Nfo $nfo): void
300
    {
301
        $this->nfo = $nfo;
302
    }
303
304
    /**
305
     * Set echo output setting.
306
     */
307
    public function setEchoOutput(bool $echo): void
308
    {
309
        $this->echoOutput = $echo;
310
    }
311
312
    /**
313
     * Get echo output setting.
314
     */
315
    public function getEchoOutput(): bool
316
    {
317
        return $this->echoOutput;
318
    }
319
}
320
321