Passed
Push — master ( 0980e4...0c25e1 )
by Darko
16:40
created

Nfo   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 389
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 57
eloc 175
dl 0
loc 389
rs 5.04
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 5
F processNfoFiles() 0 139 25
A addAlternateNfo() 0 32 6
A parseShowId() 0 29 4
C isNFO() 0 56 12
A NfoQueryString() 0 14 5

How to fix   Complexity   

Complex Class

Complex classes like Nfo often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Nfo, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Blacklight;
4
5
use App\Models\Release;
6
use App\Models\ReleaseNfo;
7
use App\Models\Settings;
8
use App\Models\UsenetGroup;
9
use Blacklight\processing\PostProcess;
10
use Blacklight\utility\Utility;
11
use dariusiii\rarinfo\Par2Info;
12
use dariusiii\rarinfo\SfvInfo;
13
use Illuminate\Support\Facades\DB;
14
use Illuminate\Support\Facades\File;
15
16
/**
17
 * Class Nfo.
18
 */
19
class Nfo
20
{
21
    /**
22
     * @var int
23
     */
24
    private $nzbs;
25
26
    /**
27
     * @var int
28
     */
29
    protected $maxSize;
30
31
    /**
32
     * @var int
33
     */
34
    private $maxRetries;
35
36
    /**
37
     * @var int
38
     */
39
    protected $minSize;
40
41
    /**
42
     * @var string
43
     */
44
    private $tmpPath;
45
46
    /**
47
     * @var bool
48
     */
49
    protected $echo;
50
51
    public const NFO_FAILED = -9; // We failed to get a NFO after admin set max retries.
52
53
    public const NFO_UNPROC = -1; // Release has not been processed yet.
54
55
    public const NFO_NONFO = 0; // Release has no NFO.
56
57
    public const NFO_FOUND = 1; // Release has an NFO.
58
59
    /**
60
     * @var \Blacklight\ColorCLI
61
     */
62
    protected ColorCLI $colorCli;
63
64
    /**
65
     * Default constructor.
66
     *
67
     * @throws \Exception
68
     */
69
    public function __construct()
70
    {
71
        $this->echo = config('nntmux.echocli');
72
        $this->nzbs = Settings::settingValue('..maxnfoprocessed') !== '' ? (int) Settings::settingValue('..maxnfoprocessed') : 100;
0 ignored issues
show
introduced by
The condition App\Models\Settings::set...axnfoprocessed') !== '' is always true.
Loading history...
73
        $this->maxRetries = (int) Settings::settingValue('..maxnforetries') >= 0 ? -((int) Settings::settingValue('..maxnforetries') + 1) : self::NFO_UNPROC;
74
        $this->maxRetries = $this->maxRetries < -8 ? -8 : $this->maxRetries;
75
        $this->maxSize = (int) Settings::settingValue('..maxsizetoprocessnfo');
76
        $this->minSize = (int) Settings::settingValue('..minsizetoprocessnfo');
77
        $this->colorCli = new ColorCLI();
78
79
        $this->tmpPath = (string) Settings::settingValue('..tmpunrarpath');
80
        if (! preg_match('/[\/\\\\]$/', $this->tmpPath)) {
81
            $this->tmpPath .= '/';
82
        }
83
    }
84
85
    /**
86
     * Look for a TV Show ID in a string.
87
     *
88
     * @param  string  $str  The string with a Show ID.
89
     * @return array|false Return array with show ID and site source or false on failure.
90
     */
91
    public function parseShowId(string $str)
92
    {
93
        $return = false;
94
95
        if (preg_match('/tvmaze\.com\/shows\/(\d{1,6})/i', $str, $hits)) {
96
            $return =
97
            [
98
                'showid' => trim($hits[1]),
99
                'site' => 'tvmaze',
100
            ];
101
        }
102
103
        if (preg_match('/imdb\.com\/title\/(tt\d{1,8})/i', $str, $hits)) {
104
            $return =
105
                [
106
                    'showid' => trim($hits[1]),
107
                    'site' => 'imdb',
108
                ];
109
        }
110
111
        if (preg_match('/thetvdb\.com\/\?tab=series&id=(\d{1,8})/i', $str, $hits)) {
112
            $return =
113
                [
114
                    'showid' => trim($hits[1]),
115
                    'site' => 'thetvdb',
116
                ];
117
        }
118
119
        return $return;
120
    }
121
122
    /**
123
     * Confirm this is an NFO file.
124
     *
125
     * @param  string|bool  $possibleNFO  The nfo.
126
     * @param  string  $guid  The guid of the release.
127
     * @return bool True on success, False on failure.
128
     *
129
     * @throws \Exception
130
     */
131
    public function isNFO(&$possibleNFO, string $guid): bool
132
    {
133
        if ($possibleNFO === false) {
134
            return false;
135
        }
136
137
        // Make sure it's not too big or small, size needs to be at least 12 bytes for header checking. Ignore common file types.
138
        $size = \strlen($possibleNFO);
0 ignored issues
show
Bug introduced by
It seems like $possibleNFO can also be of type true; however, parameter $string of strlen() 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

138
        $size = \strlen(/** @scrutinizer ignore-type */ $possibleNFO);
Loading history...
139
        if ($size < 65535 &&
140
            $size > 11 &&
141
            ! preg_match(
142
                '/\A(\s*<\?xml|=newz\[NZB\]=|RIFF|\s*[RP]AR|.{0,10}(JFIF|matroska|ftyp|ID3))|;\s*Generated\s*by.*SF\w/i',
143
                $possibleNFO
0 ignored issues
show
Bug introduced by
It seems like $possibleNFO can also be of type true; however, parameter $subject of preg_match() 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

143
                /** @scrutinizer ignore-type */ $possibleNFO
Loading history...
144
            )) {
145
            // File/GetId3 work with files, so save to disk.
146
            $tmpPath = $this->tmpPath.$guid.'.nfo';
147
            File::put($tmpPath, $possibleNFO);
0 ignored issues
show
Bug introduced by
It seems like $possibleNFO can also be of type true; however, parameter $contents of Illuminate\Support\Facades\File::put() 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

147
            File::put($tmpPath, /** @scrutinizer ignore-type */ $possibleNFO);
Loading history...
148
149
            // Linux boxes have 'file' (so should Macs), Windows *can* have it too: see GNUWIN.txt in docs.
150
            $result = Utility::fileInfo($tmpPath);
151
            if (! empty($result)) {
152
                // Check if it's text.
153
                if (preg_match('/(ASCII|ISO-8859|UTF-(8|16|32).*?)\s*text/', $result)) {
154
                    @File::delete($tmpPath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for delete(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

154
                    /** @scrutinizer ignore-unhandled */ @File::delete($tmpPath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
155
156
                    return true;
157
158
                    // Or binary.
159
                }
160
161
                if (preg_match('/^(JPE?G|Parity|PNG|RAR|XML|(7-)?[Zz]ip)/', $result) || preg_match('/[\x00-\x08\x12-\x1F\x0B\x0E\x0F]/', $possibleNFO)) {
162
                    @File::delete($tmpPath);
163
164
                    return false;
165
                }
166
            }
167
168
            // If above checks couldn't  make a categorical identification, Use GetId3 to check if it's an image/video/rar/zip etc..
169
            $check = (new \getID3())->analyze($tmpPath);
170
            @File::delete($tmpPath);
171
            if (isset($check['error'])) {
172
                // Check if it's a par2.
173
                $par2info = new Par2Info();
174
                $par2info->setData($possibleNFO);
0 ignored issues
show
Bug introduced by
It seems like $possibleNFO can also be of type true; however, parameter $data of dariusiii\rarinfo\ArchiveReader::setData() 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

174
                $par2info->setData(/** @scrutinizer ignore-type */ $possibleNFO);
Loading history...
175
                if ($par2info->error) {
176
                    // Check if it's an SFV.
177
                    $sfv = new SfvInfo();
178
                    $sfv->setData($possibleNFO);
179
                    if ($sfv->error) {
180
                        return true;
181
                    }
182
                }
183
            }
184
        }
185
186
        return false;
187
    }
188
189
    /**
190
     * Add an NFO from alternate sources. ex.: PreDB, rar, zip, etc...
191
     *
192
     * @param  string  $nfo  The nfo.
193
     * @param  NNTP  $nntp  Instance of class NNTP.
194
     * @return bool True on success, False on failure.
195
     *
196
     * @throws \Exception
197
     */
198
    public function addAlternateNfo(string &$nfo, $release, NNTP $nntp): bool
199
    {
200
        if ($release->id > 0 && $this->isNFO($nfo, $release->guid)) {
201
            $check = ReleaseNfo::whereReleasesId($release->id)->first(['releases_id']);
202
203
            if ($check === null) {
204
                ReleaseNfo::query()->insert(['releases_id' => $release->id, 'nfo' => "\x1f\x8b\x08\x00".gzcompress($nfo)]);
205
            }
206
207
            Release::whereId($release->id)->update(['nfostatus' => self::NFO_FOUND]);
208
209
            if (! isset($release->completion)) {
210
                $release->completion = 0;
211
            }
212
213
            if ($release->completion === 0) {
214
                $nzbContents = new NZBContents(
215
                    [
0 ignored issues
show
Unused Code introduced by
The call to Blacklight\NZBContents::__construct() has too many arguments starting with array('Echo' => $this->e...echo, 'Nfo' => $this))). ( Ignorable by Annotation )

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

215
                $nzbContents = /** @scrutinizer ignore-call */ new NZBContents(

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...
216
                        'Echo' => $this->echo,
217
                        'NNTP' => $nntp,
218
                        'Nfo' => $this,
219
                        'Settings' => null,
220
                        'PostProcess' => new PostProcess(['Echo' => $this->echo, 'Nfo' => $this]),
0 ignored issues
show
Unused Code introduced by
The call to Blacklight\processing\PostProcess::__construct() has too many arguments starting with array('Echo' => $this->echo, 'Nfo' => $this). ( Ignorable by Annotation )

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

220
                        'PostProcess' => /** @scrutinizer ignore-call */ new PostProcess(['Echo' => $this->echo, 'Nfo' => $this]),

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...
221
                    ]
222
                );
223
                $nzbContents->parseNZB($release->guid, $release->id, $release->guid);
224
            }
225
226
            return true;
227
        }
228
229
        return false;
230
    }
231
232
    /**
233
     * Attempt to find NFO files inside the NZB's of releases.
234
     *
235
     * @param  string  $groupID  (optional) Group ID.
236
     * @param  string  $guidChar  (optional) First character of the release GUID (used for multi-processing).
237
     * @param  int  $processImdb  (optional) Attempt to find IMDB id's in the NZB?
238
     * @param  int  $processTv  (optional) Attempt to find Tv id's in the NZB?
239
     * @return int How many NFO's were processed?
240
     *
241
     * @throws \Exception
242
     */
243
    public function processNfoFiles($nntp, string $groupID = '', string $guidChar = '', int $processImdb = 1, int $processTv = 1): int
244
    {
245
        $ret = 0;
246
247
        $qry = Release::query()
248
            ->where('nzbstatus', '=', NZB::NZB_ADDED)
249
            ->whereBetween('nfostatus', [$this->maxRetries, self::NFO_UNPROC]);
250
251
        if ($guidChar !== '') {
252
            $qry->where('leftguid', $guidChar);
253
        }
254
        if ($groupID !== '') {
255
            $qry->where('groups_id', $groupID);
256
        }
257
258
        if ($this->maxSize > 0) {
259
            $qry->where('size', '<', $this->maxSize * 1073741824);
260
        }
261
262
        if ($this->minSize > 0) {
263
            $qry->where('size', '>', $this->minSize * 1048576);
264
        }
265
266
        $res = $qry
267
            ->orderBy('nfostatus')
268
            ->orderByDesc('postdate')
269
            ->limit($this->nzbs)
270
            ->get(['id', 'guid', 'groups_id', 'name']);
271
272
        $nfoCount = $res->count();
273
274
        if ($nfoCount > 0) {
275
            $this->colorCli->primary(
276
                PHP_EOL.
277
                    ($guidChar === '' ? '' : '['.$guidChar.'] ').
278
                    ($groupID === '' ? '' : '['.$groupID.'] ').
279
                    'Processing '.$nfoCount.
280
                    ' NFO(s), starting at '.$this->nzbs.
281
                    ' * = hidden NFO, + = NFO, - = no NFO, f = download failed.'
282
            );
283
284
            if ($this->echo) {
285
                // Get count of releases per nfo status
286
                $qry = Release::query()
287
                    ->where('nzbstatus', '=', NZB::NZB_ADDED)
288
                    ->whereBetween('nfostatus', [$this->maxRetries, self::NFO_UNPROC])
289
                    ->select(['nfostatus as status', DB::raw('COUNT(id) as count')])
290
                    ->groupBy(['nfostatus'])
291
                    ->orderBy('nfostatus');
292
293
                if ($guidChar !== '') {
294
                    $qry->where('leftguid', $guidChar);
295
                }
296
                if ($groupID !== '') {
297
                    $qry->where('groups_id', $groupID);
298
                }
299
300
                if ($this->maxSize > 0) {
301
                    $qry->where('size', '<', $this->maxSize * 1073741824);
302
                }
303
304
                if ($this->minSize > 0) {
305
                    $qry->where('size', '>', $this->minSize * 1048576);
306
                }
307
308
                $nfoStats = $qry->get();
309
310
                if ($nfoStats instanceof \Traversable) {
311
                    $outString = PHP_EOL.'Available to process';
312
                    foreach ($nfoStats as $row) {
313
                        $outString .= ', '.$row['status'].' = '.number_format($row['count']);
314
                    }
315
                    $this->colorCli->header($outString.'.');
316
                }
317
            }
318
319
            $nzbContents = new NZBContents(
320
                [
0 ignored issues
show
Unused Code introduced by
The call to Blacklight\NZBContents::__construct() has too many arguments starting with array('Echo' => $this->e...echo, 'Nfo' => $this))). ( Ignorable by Annotation )

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

320
            $nzbContents = /** @scrutinizer ignore-call */ new NZBContents(

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...
321
                    'Echo' => $this->echo,
322
                    'NNTP' => $nntp,
323
                    'Nfo' => $this,
324
                    'Settings' => null,
325
                    'PostProcess' => new PostProcess(['Echo' => $this->echo, 'Nfo' => $this]),
0 ignored issues
show
Unused Code introduced by
The call to Blacklight\processing\PostProcess::__construct() has too many arguments starting with array('Echo' => $this->echo, 'Nfo' => $this). ( Ignorable by Annotation )

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

325
                    'PostProcess' => /** @scrutinizer ignore-call */ new PostProcess(['Echo' => $this->echo, 'Nfo' => $this]),

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...
326
                ]
327
            );
328
            $movie = new Movie(['Echo' => $this->echo]);
0 ignored issues
show
Unused Code introduced by
The call to Blacklight\Movie::__construct() has too many arguments starting with array('Echo' => $this->echo). ( Ignorable by Annotation )

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

328
            $movie = /** @scrutinizer ignore-call */ new Movie(['Echo' => $this->echo]);

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...
329
330
            foreach ($res as $arr) {
331
                $fetchedBinary = $nzbContents->getNfoFromNZB($arr['guid'], $arr['id'], $arr['groups_id'], UsenetGroup::getNameByID($arr['groups_id']));
332
                if ($fetchedBinary !== false) {
333
                    // Insert nfo into database.
334
335
                    $ckReleaseId = ReleaseNfo::whereReleasesId($arr['id'])->first(['releases_id']);
336
                    if ($ckReleaseId === null) {
337
                        ReleaseNfo::query()->insert(['releases_id' => $arr['id'], 'nfo' => "\x1f\x8b\x08\x00".gzcompress($fetchedBinary)]);
338
                    }
339
                    Release::whereId($arr['id'])->update(['nfostatus' => self::NFO_FOUND]);
340
                    $ret++;
341
                    $movie->doMovieUpdate($fetchedBinary, 'nfo', $arr['id'], $processImdb);
342
343
                    // If set scan for tv info.
344
                    if ($processTv === 1) {
345
                        (new PostProcess(['Echo' => $this->echo]))->processTv($groupID, $guidChar, $processTv);
346
                    }
347
                }
348
            }
349
        }
350
351
        // Remove nfo that we cant fetch after 5 attempts.
352
        $qry = Release::query()
353
            ->where('nzbstatus', NZB::NZB_ADDED)
354
            ->where('nfostatus', '<', $this->maxRetries)
355
            ->where('nfostatus', '>', self::NFO_FAILED);
356
357
        if ($guidChar !== '') {
358
            $qry->where('leftguid', $guidChar);
359
        }
360
        if ($groupID !== '') {
361
            $qry->where('groups_id', $groupID);
362
        }
363
364
        foreach ($qry->get(['id']) as $release) {
365
            // remove any releasenfo for failed
366
            ReleaseNfo::whereReleasesId($release['id'])->delete();
367
368
            // set release.nfostatus to failed
369
            Release::whereId($release['id'])->update(['nfostatus' => self::NFO_FAILED]);
370
        }
371
372
        if ($this->echo) {
373
            if ($nfoCount > 0) {
374
                echo PHP_EOL;
375
            }
376
            if ($ret > 0) {
377
                $this->colorCli->primary($ret.' NFO file(s) found/processed.');
378
            }
379
        }
380
381
        return $ret;
382
    }
383
384
    /**
385
     * Get a string like this:
386
     * "AND r.nzbstatus = 1 AND r.nfostatus BETWEEN -8 AND -1 AND r.size < 1073741824 AND r.size > 1048576"
387
     * To use in a query.
388
     *
389
     *
390
     * @throws \Exception
391
     *
392
     * @static
393
     */
394
    public static function NfoQueryString(): string
395
    {
396
        $maxSize = (int) Settings::settingValue('..maxsizetoprocessnfo');
397
        $minSize = (int) Settings::settingValue('..minsizetoprocessnfo');
398
        $dummy = (int) Settings::settingValue('..maxnforetries');
399
        $maxRetries = ($dummy >= 0 ? -($dummy + 1) : self::NFO_UNPROC);
400
401
        return sprintf(
402
            'AND r.nzbstatus = %d AND r.nfostatus BETWEEN %d AND %d %s %s',
403
            NZB::NZB_ADDED,
404
            ($maxRetries < -8 ? -8 : $maxRetries),
405
            self::NFO_UNPROC,
406
            ($maxSize > 0 ? ('AND r.size < '.($maxSize * 1073741824)) : ''),
407
            ($minSize > 0 ? ('AND r.size > '.($minSize * 1048576)) : '')
408
        );
409
    }
410
}
411