NzbImportService::beginImport()   F
last analyzed

Complexity

Conditions 26
Paths 1172

Size

Total Lines 138
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 79
c 2
b 0
f 0
dl 0
loc 138
rs 0
cc 26
nc 1172
nop 5

How to fix   Long Method    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\Models\UsenetGroup;
10
use App\Services\BlacklistService;
11
use App\Services\Categorization\CategorizationService;
12
use App\Services\ReleaseCleaningService;
0 ignored issues
show
Bug introduced by
The type App\Services\ReleaseCleaningService was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use Illuminate\Support\Carbon;
14
use Illuminate\Support\Facades\File;
15
use Illuminate\Support\Str;
16
17
/**
18
 * Service for importing NZB files into the database.
19
 */
20
class NzbImportService
21
{
22
    protected BlacklistService $blacklistService;
23
24
    protected ReleaseCleaningService $releaseCleaner;
25
26
    protected \stdClass|bool $site;
27
28
    protected mixed $crossPostt;
29
30
    protected CategorizationService $category;
31
32
    /**
33
     * List of all the group names/ids in the DB.
34
     */
35
    protected array $allGroups;
36
37
    /**
38
     * Was this run from the browser?
39
     */
40
    protected bool $browser;
41
42
    /**
43
     * Return value for browser.
44
     */
45
    protected string $retVal;
46
47
    /**
48
     * Guid of the current releases.
49
     */
50
    protected string $relGuid;
51
52
    public mixed $echoCLI;
53
54
    public NzbService $nzb;
55
56
    /**
57
     * The MD5 hash of the first segment Message-ID of the NZB
58
     */
59
    protected string $nzbGuid;
60
61
    public function __construct(array $options = [])
62
    {
63
        $this->echoCLI = config('nntmux.echocli');
64
        $this->blacklistService = new BlacklistService;
65
        $this->category = new CategorizationService();
66
        $this->nzb = app(NzbService::class);
67
        $this->releaseCleaner = new ReleaseCleaningService;
68
        $this->crossPostt = Settings::settingValue('crossposttime') !== '' ? Settings::settingValue('crossposttime') : 2;
69
70
        // Set properties from options
71
        $this->browser = isset($options['Browser']) ? (bool) $options['Browser'] : false;
72
        $this->retVal = '';
73
    }
74
75
    /**
76
     * Begin importing NZB files.
77
     *
78
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
79
     */
80
    public function beginImport($filesToProcess, bool $useNzbName = false, bool $delete = false, bool $deleteFailed = false, int $source = 1): bool|string
81
    {
82
        // Get all the groups in the DB.
83
        if (! $this->getAllGroups()) {
84
            if ($this->browser) {
85
                return $this->retVal;
86
            }
87
88
            return false;
89
        }
90
91
        $start = now()->toImmutable()->format('Y-m-d H:i:s');
92
        $nzbsImported = $nzbsSkipped = 0;
93
94
        // Convert all files to string paths and filter to only process NZB files
95
        $nzbFiles = [];
96
        foreach ($filesToProcess as $file) {
97
            $filePath = $file instanceof \SplFileInfo ? $file->getPathname() : (string) $file;
98
            if (Str::endsWith($filePath, '.nzb') || Str::endsWith($filePath, '.nzb.gz')) {
99
                $nzbFiles[] = $filePath;
100
            }
101
        }
102
103
        if (empty($nzbFiles)) {
104
            $this->echoOut('No NZB files found to process.');
105
            if ($this->browser) {
106
                return $this->retVal;
107
            }
108
109
            return false;
110
        }
111
112
        $totalFilesFiltered = count($filesToProcess) - count($nzbFiles);
113
        if ($totalFilesFiltered > 0) {
114
            $this->echoOut("Filtered out {$totalFilesFiltered} non-NZB files. Processing ".count($nzbFiles).' NZB files.');
115
        }
116
117
        // Loop over the NZB file names only.
118
        foreach ($nzbFiles as $nzbFilePath) {
119
            $this->nzbGuid = '';
120
121
122
            // Check if the file is really there.
123
            if (File::isFile($nzbFilePath)) {
124
                // Get the contents of the NZB file as a string.
125
                if (Str::endsWith($nzbFilePath, '.nzb.gz')) {
126
                    $nzbString = unzipGzipFile($nzbFilePath);
127
                } else {
128
                    $nzbString = File::get($nzbFilePath);
129
                }
130
131
                if ($nzbString === false) {
132
                    $this->echoOut('ERROR: Unable to read: '.$nzbFilePath);
133
134
                    if ($deleteFailed) {
135
                        File::delete($nzbFilePath);
136
                    }
137
                    $nzbsSkipped++;
138
139
                    continue;
140
                }
141
142
                // Load it as an XML object.
143
                $nzbXML = @simplexml_load_string($nzbString);
144
                if ($nzbXML === false || strtolower($nzbXML->getName()) !== 'nzb') {
145
                    $this->echoOut('ERROR: Unable to load NZB XML data: '.$nzbFilePath);
146
147
                    if ($deleteFailed) {
148
                        File::delete($nzbFilePath);
149
                    }
150
                    $nzbsSkipped++;
151
152
                    continue;
153
                }
154
155
                // Try to insert the NZB details into the DB.
156
                $nzbFileName = $useNzbName === true ? str_ireplace('.nzb', '', basename($nzbFilePath)) : '';
157
                try {
158
                    $inserted = $this->scanNZBFile($nzbXML, $nzbFileName, $source);
0 ignored issues
show
Unused Code introduced by
The call to App\Services\Nzb\NzbImportService::scanNZBFile() has too many arguments starting with $source. ( Ignorable by Annotation )

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

158
                    /** @scrutinizer ignore-call */ 
159
                    $inserted = $this->scanNZBFile($nzbXML, $nzbFileName, $source);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
159
                } catch (\Exception $e) {
160
                    $this->echoOut('ERROR: Problem inserting: '.$nzbFilePath);
161
                    $inserted = false;
162
                }
163
164
                if ($inserted) {
165
                    // Try to copy the NZB to the NZB folder.
166
                    $path = $this->nzb->getNzbPath($this->relGuid, 0, true);
167
168
                    // Try to compress the NZB file in the NZB folder.
169
                    $fp = gzopen($path, 'w5');
170
                    gzwrite($fp, $nzbString);
171
                    gzclose($fp);
172
173
                    if (! File::isFile($path)) {
174
                        $this->echoOut('ERROR: Problem compressing NZB file to: '.$path);
175
176
                        // Remove the release.
177
                        Release::query()->where('guid', $this->relGuid)->delete();
178
179
                        if ($deleteFailed) {
180
                            File::delete($nzbFilePath);
181
                        }
182
                        $nzbsSkipped++;
183
                    } else {
184
                        $this->updateNzbGuid();
185
                        if ($delete) {
186
                            // Remove the nzb file.
187
                            File::delete($nzbFilePath);
188
                        }
189
190
                        $nzbsImported++;
191
                    }
192
                } else {
193
                    $this->echoOut('ERROR: Failed to insert NZB!');
194
                    if ($deleteFailed) {
195
                        File::delete($nzbFilePath);
196
                    }
197
                    $nzbsSkipped++;
198
                }
199
            } else {
200
                $this->echoOut('ERROR: Unable to fetch: '.$nzbFilePath);
201
                $nzbsSkipped++;
202
            }
203
        }
204
        $this->echoOut(
205
            'Proccessed '.
206
            $nzbsImported.
207
            ' NZBs in '.
208
            now()->diffInSeconds($start, true).' seconds, '.
209
            $nzbsSkipped.
210
            ' NZBs were skipped.'
211
        );
212
213
        if ($this->browser) {
214
            return $this->retVal;
215
        }
216
217
        return true;
218
    }
219
220
    /**
221
     * Scan and process an NZB file.
222
     *
223
     * @throws \Exception
224
     */
225
    protected function scanNZBFile(&$nzbXML, $nzbFileName = ''): bool
226
    {
227
        $binary_names = [];
228
        $totalFiles = $totalSize = $groupID = 0;
229
        $isBlackListed = $groupName = $firstName = $posterName = $postDate = false;
230
231
        // Go through the NZB, get the details, look if it's blacklisted, look if we have the groups.
232
        foreach ($nzbXML->file as $file) {
233
            $binary_names[] = $file['subject'];
234
            $totalFiles++;
235
            $groupID = -1;
236
237
            // Get the nzb info.
238
            if ($firstName === false) {
239
                $firstName = (string) $file->attributes()->subject;
240
            }
241
            if ($posterName === false) {
242
                $posterName = (string) $file->attributes()->poster;
243
            }
244
            if ($postDate === false) {
245
                $postDate = Carbon::createFromTimestamp((string) $file->attributes()->date, date_default_timezone_get())->format('Y-m-d H:i:s');
246
            }
247
248
            // Make a fake message array to use to check the blacklist.
249
            $msg = ['Subject' => (string) $file->attributes()->subject, 'From' => (string) $file->attributes()->poster, 'Message-ID' => ''];
250
251
            // Get the group names, group_id, check if it's blacklisted.
252
            $groupArr = [];
253
            foreach ($file->groups->group as $group) {
254
                $group = (string) $group;
255
256
                // If group_id is -1 try to get a group_id.
257
                if ($groupID === -1) {
258
                    if (array_key_exists($group, $this->allGroups)) {
259
                        $groupID = $this->allGroups[$group];
260
                        if (! $groupName) {
261
                            $groupName = $group;
262
                        }
263
                    } else {
264
                        $group = UsenetGroup::isValidGroup($group);
265
                        if ($group !== false) {
266
                            $groupID = UsenetGroup::addGroup([
267
                                'name' => $group,
268
                                'description' => 'Added by NZBimport script.',
269
                                'backfill_target' => 1,
270
                                'minfilestoformrelease' => '',
271
                                'minsizetoformrelease' => '',
272
                                'first_record' => 0,
273
                                'last_record' => 0,
274
                                'active' => 0,
275
                                'backfill' => 0,
276
                            ]);
277
                            $this->allGroups[$group] = $groupID;
278
279
                            $this->echoOut("Adding missing group: ($group)");
280
                        }
281
                    }
282
                }
283
                // Add all the found groups to an array.
284
                $groupArr[] = $group;
285
286
                // Check if this NZB is blacklisted.
287
                if ($this->blacklistService->isBlackListed($msg, $group)) {
288
                    $isBlackListed = true;
289
                    break;
290
                }
291
            }
292
293
            // If we found a group and it's not blacklisted.
294
            if ($groupID !== -1 && ! $isBlackListed) {
295
                // Get the size of the release.
296
                if (\count($file->segments->segment) > 0) {
297
                    foreach ($file->segments->segment as $segment) {
298
                        $totalSize += (int) $segment->attributes()->bytes;
299
                    }
300
                }
301
            } else {
302
                if ($isBlackListed) {
303
                    $errorMessage = 'Subject is blacklisted: '.mb_convert_encoding(trim($firstName), 'UTF-8', mb_list_encodings());
0 ignored issues
show
Bug introduced by
Are you sure mb_convert_encoding(trim...', mb_list_encodings()) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

303
                    $errorMessage = 'Subject is blacklisted: './** @scrutinizer ignore-type */ mb_convert_encoding(trim($firstName), 'UTF-8', mb_list_encodings());
Loading history...
Bug introduced by
It seems like $firstName can also be of type false; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

303
                    $errorMessage = 'Subject is blacklisted: '.mb_convert_encoding(trim(/** @scrutinizer ignore-type */ $firstName), 'UTF-8', mb_list_encodings());
Loading history...
304
                } else {
305
                    $errorMessage = 'No group found for '.$firstName.' (one of '.implode(', ', $groupArr).' are missing';
0 ignored issues
show
Bug introduced by
Are you sure $firstName of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

305
                    $errorMessage = 'No group found for './** @scrutinizer ignore-type */ $firstName.' (one of '.implode(', ', $groupArr).' are missing';
Loading history...
306
                }
307
                $this->echoOut($errorMessage);
308
309
                // Persist blacklist usage stats if we matched any rule during this NZB processing
310
                $this->blacklistService->updateBlacklistUsage($this->blacklistService->getAndClearIdsToUpdate());
311
312
                return false;
313
            }
314
        }
315
316
        // After scanning all files, persist any matched whitelist/blacklist usage
317
        $this->blacklistService->updateBlacklistUsage($this->blacklistService->getAndClearIdsToUpdate());
318
319
        // Sort values alphabetically but keep the keys intact
320
        if (\count($binary_names) > 0) {
321
            asort($binary_names);
322
            foreach ($nzbXML->file as $file) {
323
                if ($file['subject'] === $binary_names[0]) {
324
                    $this->nzbGuid = md5($file->segments->segment);
325
                    break;
326
                }
327
            }
328
        }
329
330
        // Try to insert the NZB details into the DB.
331
        return $this->insertNZB(
332
            [
333
                'subject' => $firstName,
334
                'useFName' => $nzbFileName,
335
                'postDate' => empty($postDate) ? now()->format('Y-m-d H:i:s') : $postDate,
336
                'from' => empty($posterName) ? '' : $posterName,
337
                'groups_id' => $groupID,
338
                'groupName' => $groupName,
339
                'totalFiles' => $totalFiles,
340
                'totalSize' => $totalSize,
341
            ]
342
        );
343
    }
344
345
    /**
346
     * Insert the NZB details into the database.
347
     *
348
     * @throws \Exception
349
     */
350
    protected function insertNZB($nzbDetails): bool
351
    {
352
        // Make up a GUID for the release.
353
        $this->relGuid = Str::uuid()->toString();
354
355
        // Remove part count from subject.
356
        $partLess = preg_replace('/(\(\d+\/\d+\))*$/', 'yEnc', $nzbDetails['subject']);
357
        // Remove added yEnc from above and anything after.
358
        $subject = mb_convert_encoding(trim(preg_replace('/yEnc.*$/i', 'yEnc', $partLess)), 'UTF-8', mb_list_encodings());
359
360
        $renamed = 0;
361
        if ($nzbDetails['useFName'] !== '') {
362
            // If we are using the filename as the subject. We don't need to clean it.
363
            $cleanName = $nzbDetails['useFName'];
364
            $renamed = 1;
365
        } else {
366
            // Pass the subject through release cleaner to get a nicer name.
367
            $cleanName = $this->releaseCleaner->releaseCleaner($subject, $nzbDetails['from'], $nzbDetails['groupName']);
368
            if (isset($cleanName['properlynamed'])) {
369
                $cleanName = $cleanName['cleansubject'];
370
                $renamed = (isset($cleanName['properlynamed']) && $cleanName['properlynamed'] === true ? 1 : 0);
371
            }
372
        }
373
374
        $escapedSubject = $subject;
375
        $escapedFromName = $nzbDetails['from'];
376
377
        // Look for a duplicate on name, poster and size.
378
        $dupeCheck = Release::query()->where(['name' => $escapedSubject, 'fromname' => $escapedFromName])->whereBetween('size', [$nzbDetails['totalSize'] * 0.99, $nzbDetails['totalSize'] * 1.01])->first(['id']);
379
380
        if ($dupeCheck === null) {
381
            $escapedSearchName = $cleanName;
382
            $determinedCategory = $this->category->determineCategory($nzbDetails['groups_id'], $cleanName, $escapedFromName);
383
            // Insert the release into the DB.
384
            $relID = Release::insertRelease(
385
                [
386
                    'name' => $escapedSubject,
387
                    'searchname' => $escapedSearchName ?? $escapedSubject,
388
                    'totalpart' => $nzbDetails['totalFiles'],
389
                    'groups_id' => $nzbDetails['groups_id'],
390
                    'guid' => $this->relGuid,
391
                    'postdate' => $nzbDetails['postDate'],
392
                    'fromname' => $escapedFromName,
393
                    'size' => $nzbDetails['totalSize'],
394
                    'categories_id' => $determinedCategory['categories_id'],
395
                    'isrenamed' => $renamed,
396
                    'predb_id' => 0,
397
                    'nzbstatus' => NzbService::NZB_ADDED,
398
                    'ishashed' => 0,
399
                ]
400
            );
401
        } else {
402
            $this->echoOut('This release is already in our DB so skipping: '.$subject);
0 ignored issues
show
Bug introduced by
Are you sure $subject of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

402
            $this->echoOut('This release is already in our DB so skipping: './** @scrutinizer ignore-type */ $subject);
Loading history...
403
404
            return false;
405
        }
406
407
        if ($relID === null) {
0 ignored issues
show
introduced by
The condition $relID === null is always false.
Loading history...
408
            $this->echoOut('ERROR: Problem inserting: '.$subject);
409
410
            return false;
411
        }
412
413
        return true;
414
    }
415
416
    /**
417
     * Get all groups in the DB.
418
     */
419
    protected function getAllGroups(): bool
420
    {
421
        $this->allGroups = [];
422
        $groups = UsenetGroup::query()->get(['id', 'name']);
423
424
        if ($groups instanceof \Traversable) {
0 ignored issues
show
introduced by
$groups is always a sub-type of Traversable.
Loading history...
425
            foreach ($groups as $group) {
426
                $this->allGroups[$group['name']] = $group['id'];
427
            }
428
        }
429
430
        if (\count($this->allGroups) === 0) {
431
            $this->echoOut('You have no groups in your database!');
432
433
            return false;
434
        }
435
436
        return true;
437
    }
438
439
    /**
440
     * Echo message to browser or CLI.
441
     */
442
    protected function echoOut(string $message): void
443
    {
444
        if ($this->browser) {
445
            $this->retVal .= $message.'<br />';
446
        } elseif ($this->echoCLI) {
447
            cli()->notice($message);
448
        }
449
    }
450
451
    /**
452
     * The function updates the NZB guid after there is no chance of deletion.
453
     */
454
    protected function updateNzbGuid(): void
455
    {
456
        try {
457
            Release::query()->where('guid', $this->relGuid)->update(['nzb_guid' => sodium_hex2bin($this->nzbGuid)]);
458
        } catch (\SodiumException $e) {
459
            $this->echoOut('ERROR: Problem updating nzb_guid for: '.$this->relGuid);
460
        }
461
    }
462
}
463
464