Completed
Branch dev (4bcb34)
by Darko
13:52
created

Nfo::processNfoFiles()   F

Complexity

Conditions 25
Paths > 20000

Size

Total Lines 144
Code Lines 86

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 25
eloc 86
nc 21760
nop 5
dl 0
loc 144
rs 2
c 0
b 0
f 0

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
namespace Blacklight;
4
5
use App\Models\Group;
6
use App\Models\Release;
7
use App\Models\Settings;
8
use App\Models\ReleaseNfo;
9
use dariusiii\rarinfo\SfvInfo;
10
use Blacklight\utility\Utility;
11
use dariusiii\rarinfo\Par2Info;
12
use Blacklight\processing\PostProcess;
13
14
/**
15
 * Class Nfo
16
 * Class for handling fetching/storing of NFO files.
17
 */
18
class Nfo
19
{
20
    /**
21
     * @var \Blacklight\db\DB
22
     */
23
    public $pdo;
24
25
    /**
26
     * @var int
27
     */
28
    private $nzbs;
29
30
    /**
31
     * @var int
32
     */
33
    protected $maxSize;
34
35
    /**
36
     * @var int
37
     */
38
    private $maxRetries;
39
40
    /**
41
     * @var int
42
     */
43
    protected $minSize;
44
45
    /**
46
     * @var string
47
     */
48
    private $tmpPath;
49
50
    /**
51
     * @var bool
52
     */
53
    protected $echo;
54
55
    public const NFO_FAILED = -9; // We failed to get a NFO after admin set max retries.
56
    public const NFO_UNPROC = -1; // Release has not been processed yet.
57
    public const NFO_NONFO = 0; // Release has no NFO.
58
    public const NFO_FOUND = 1; // Release has an NFO.
59
60
    /**
61
     * Default constructor.
62
     *
63
     * @throws \Exception
64
     */
65
    public function __construct()
66
    {
67
        $this->echo = config('nntmux.echocli');
68
        $this->nzbs = Settings::settingValue('..maxnfoprocessed') !== '' ? (int) Settings::settingValue('..maxnfoprocessed') : 100;
0 ignored issues
show
Bug introduced by
'..maxnfoprocessed' of type string is incompatible with the type boolean|array expected by parameter $setting of App\Models\Settings::settingValue(). ( Ignorable by Annotation )

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

68
        $this->nzbs = Settings::settingValue(/** @scrutinizer ignore-type */ '..maxnfoprocessed') !== '' ? (int) Settings::settingValue('..maxnfoprocessed') : 100;
Loading history...
69
        $this->maxRetries = (int) Settings::settingValue('..maxnforetries') >= 0 ? -((int) Settings::settingValue('..maxnforetries') + 1) : self::NFO_UNPROC;
70
        $this->maxRetries = $this->maxRetries < -8 ? -8 : $this->maxRetries;
71
        $this->maxSize = (int) Settings::settingValue('..maxsizetoprocessnfo');
72
        $this->minSize = (int) Settings::settingValue('..minsizetoprocessnfo');
73
74
        $this->tmpPath = (string) Settings::settingValue('..tmpunrarpath');
75
        if (! preg_match('/[\/\\\\]$/', $this->tmpPath)) {
76
            $this->tmpPath .= DS;
0 ignored issues
show
Bug introduced by
The constant Blacklight\DS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
77
        }
78
    }
79
80
    /**
81
     * Look for a TV Show ID in a string.
82
     *
83
     * @param string  $str   The string with a Show ID.
84
     *
85
     * @return array|bool   Return array with show ID and site source or false on failure.
86
     */
87
    public function parseShowId($str)
88
    {
89
        $return = false;
90
91
        if (preg_match('/tvmaze\.com\/shows\/(\d{1,6})/i', $str, $matches)) {
92
            $return =
93
            [
94
                'showid' => trim($matches[1]),
95
                'site'   => 'tvmaze',
96
            ];
97
        }
98
99
        if (preg_match('/imdb\.com\/title\/(tt\d{1,8})/i', $str, $matches)) {
100
            $return =
101
                [
102
                    'showid' => trim($matches[1]),
103
                    'site'   => 'imdb',
104
                ];
105
        }
106
107
        if (preg_match('/thetvdb\.com\/\?tab=series&id=(\d{1,8})/i', $str, $matches)) {
108
            $return =
109
                [
110
                    'showid' => trim($matches[1]),
111
                    'site'   => 'thetvdb',
112
                ];
113
        }
114
115
        return $return;
116
    }
117
118
    /**
119
     * Confirm this is an NFO file.
120
     *
121
     * @param string $possibleNFO The nfo.
122
     * @param string $guid        The guid of the release.
123
     *
124
     * @return bool               True on success, False on failure.
125
     * @throws \Exception
126
     */
127
    public function isNFO(&$possibleNFO, $guid): bool
128
    {
129
        if ($possibleNFO === false) {
0 ignored issues
show
introduced by
The condition $possibleNFO === false is always false.
Loading history...
130
            return false;
131
        }
132
133
        // Make sure it's not too big or small, size needs to be at least 12 bytes for header checking. Ignore common file types.
134
        $size = \strlen($possibleNFO);
135
        if ($size < 65535 &&
136
            $size > 11 &&
137
            ! preg_match(
138
                '/\A(\s*<\?xml|=newz\[NZB\]=|RIFF|\s*[RP]AR|.{0,10}(JFIF|matroska|ftyp|ID3))|;\s*Generated\s*by.*SF\w/i',
139
                $possibleNFO
140
            )) {
141
            // File/GetId3 work with files, so save to disk.
142
            $tmpPath = $this->tmpPath.$guid.'.nfo';
143
            file_put_contents($tmpPath, $possibleNFO);
144
145
            // Linux boxes have 'file' (so should Macs), Windows *can* have it too: see GNUWIN.txt in docs.
146
            $result = Utility::fileInfo($tmpPath);
147
            if (! empty($result)) {
148
149
                // Check if it's text.
150
                if (preg_match('/(ASCII|ISO-8859|UTF-(8|16|32).*?)\s*text/', $result)) {
151
                    @unlink($tmpPath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

151
                    /** @scrutinizer ignore-unhandled */ @unlink($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...
152
153
                    return true;
154
155
                    // Or binary.
156
                }
157
158
                if (preg_match('/^(JPE?G|Parity|PNG|RAR|XML|(7-)?[Zz]ip)/', $result) || preg_match('/[\x00-\x08\x12-\x1F\x0B\x0E\x0F]/', $possibleNFO)) {
159
                    @unlink($tmpPath);
160
161
                    return false;
162
                }
163
            }
164
165
            // If above checks couldn't  make a categorical identification, Use GetId3 to check if it's an image/video/rar/zip etc..
166
            $check = (new \getID3())->analyze($tmpPath);
167
            @unlink($tmpPath);
168
            if (isset($check['error'])) {
169
170
                // Check if it's a par2.
171
                $par2info = new Par2Info();
172
                $par2info->setData($possibleNFO);
173
                if ($par2info->error) {
174
                    // Check if it's an SFV.
175
                    $sfv = new SfvInfo();
176
                    $sfv->setData($possibleNFO);
177
                    if ($sfv->error) {
178
                        return true;
179
                    }
180
                }
181
            }
182
        }
183
184
        return false;
185
    }
186
187
    /**
188
     * Add an NFO from alternate sources. ex.: PreDB, rar, zip, etc...
189
     *
190
     * @param string $nfo     The nfo.
191
     * @param array  $release The SQL row for this release.
192
     * @param NNTP   $nntp    Instance of class NNTP.
193
     *
194
     * @return bool           True on success, False on failure.
195
     * @throws \Exception
196
     */
197
    public function addAlternateNfo(&$nfo, $release, $nntp): bool
198
    {
199
        if ($release['id'] > 0 && $this->isNFO($nfo, $release['guid'])) {
200
            $check = ReleaseNfo::query()->where('releases_id', $release['id'])->first(['releases_id']);
201
202
            if ($check === null) {
203
                ReleaseNfo::query()->insert(['releases_id' => $release['id'], 'nfo' => "\x1f\x8b\x08\x00".gzcompress($nfo)]);
204
            }
205
206
            Release::query()->where('id', $release['id'])->update(['nfostatus' => self::NFO_FOUND]);
207
208
            if (! isset($release['completion'])) {
209
                $release['completion'] = 0;
210
            }
211
212
            if ($release['completion'] === 0) {
213
                $nzbContents = new NZBContents(
214
                    [
215
                        'Echo' => $this->echo,
216
                        'NNTP' => $nntp,
217
                        'Nfo'  => $this,
218
                        'Settings'   => null,
219
                        'PostProcess'   => new PostProcess(['Echo' => $this->echo, 'Nfo' => $this]),
220
                    ]
221
                );
222
                $nzbContents->parseNZB($release['guid'], $release['id'], $release['groups_id']);
223
            }
224
225
            return true;
226
        }
227
228
        return false;
229
    }
230
231
    /**
232
     * Attempt to find NFO files inside the NZB's of releases.
233
     *
234
     * @param        $nntp
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
     *
240
     * @return int How many NFO's were processed?
241
     * @throws \Exception
242
     */
243
    public function processNfoFiles($nntp, $groupID = '', $guidChar = '', $processImdb = 1, $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
            ->orderBy('postdate', 'desc')
269
            ->limit($this->nzbs)
270
            ->get(['id', 'guid', 'groups_id', 'name']);
271
272
        $nfoCount = $res->count();
273
274
        if ($nfoCount > 0) {
275
            ColorCLI::doEcho(
276
                ColorCLI::primary(
277
                    PHP_EOL.
278
                    ($guidChar === '' ? '' : '['.$guidChar.'] ').
279
                    ($groupID === '' ? '' : '['.$groupID.'] ').
280
                    'Processing '.$nfoCount.
281
                    ' NFO(s), starting at '.$this->nzbs.
282
                    ' * = hidden NFO, + = NFO, - = no NFO, f = download failed.'
283
                ), true
284
            );
285
286
            if ($this->echo) {
287
                // Get count of releases per nfo status
288
                $qry = Release::query()
289
                    ->where('nzbstatus', '=', NZB::NZB_ADDED)
290
                    ->whereBetween('nfostatus', [$this->maxRetries, self::NFO_UNPROC])
291
                    ->select('nfostatus as status')
292
                    ->selectRaw('COUNT(id) as count')
293
                    ->groupBy(['nfostatus'])
294
                    ->orderBy('nfostatus');
295
296
                if ($guidChar !== '') {
297
                    $qry->where('leftguid', $guidChar);
298
                }
299
                if ($groupID !== '') {
300
                    $qry->where('groups_id', $groupID);
301
                }
302
303
                if ($this->maxSize > 0) {
304
                    $qry->where('size', '<', $this->maxSize * 1073741824);
305
                }
306
307
                if ($this->minSize > 0) {
308
                    $qry->where('size', '>', $this->minSize * 1048576);
309
                }
310
311
                $nfoStats = $qry->get();
312
313
                if ($nfoStats instanceof \Traversable) {
314
                    $outString = PHP_EOL.'Available to process';
315
                    foreach ($nfoStats as $row) {
316
                        $outString .= ', '.$row['status'].' = '.number_format($row['count']);
317
                    }
318
                    ColorCLI::doEcho(ColorCLI::header($outString.'.'), true);
319
                }
320
            }
321
322
            $nzbContents = new NZBContents(
323
                [
324
                    'Echo' => $this->echo,
325
                    'NNTP' => $nntp,
326
                    'Nfo' => $this,
327
                    'Settings' => null,
328
                    'PostProcess' => new PostProcess(['Echo' => $this->echo, 'Nfo' => $this]),
329
                ]
330
            );
331
            $movie = new Movie(['Echo' => $this->echo]);
332
333
            foreach ($res as $arr) {
334
                $fetchedBinary = $nzbContents->getNfoFromNZB($arr['guid'], $arr['id'], $arr['groups_id'], Group::getNameByID($arr['groups_id']));
335
                if ($fetchedBinary !== false) {
336
                    // Insert nfo into database.
337
338
                    $ckReleaseId = ReleaseNfo::query()->where('releases_id', $arr['id'])->first(['releases_id']);
339
                    if ($ckReleaseId === null) {
340
                        ReleaseNfo::query()->insert(['releases_id' => $arr['id'], 'nfo' => "\x1f\x8b\x08\x00".gzcompress($fetchedBinary)]);
341
                    }
342
                    Release::query()->where('id', $arr['id'])->update(['nfostatus' => self::NFO_FOUND]);
343
                    $ret++;
344
                    $movie->doMovieUpdate($fetchedBinary, 'nfo', $arr['id'], $processImdb);
345
346
                    // If set scan for tv info.
347
                    if ($processTv === 1) {
348
                        (new PostProcess(['Echo' => $this->echo]))->processTv($groupID, $guidChar, $processTv);
349
                    }
350
                }
351
            }
352
        }
353
354
        // Remove nfo that we cant fetch after 5 attempts.
355
        $qry = Release::query()
356
            ->where('nzbstatus', NZB::NZB_ADDED)
357
            ->where('nfostatus', '<', $this->maxRetries)
358
            ->where('nfostatus', '>', self::NFO_FAILED);
359
360
        if ($guidChar !== '') {
361
            $qry->where('leftguid', $guidChar);
362
        }
363
        if ($groupID !== '') {
364
            $qry->where('groups_id', $groupID);
365
        }
366
367
        $releases = $qry->get(['id']);
368
369
        foreach ($releases as $release) {
370
            // remove any releasenfo for failed
371
            ReleaseNfo::query()->where('releases_id', $release['id'])->delete();
372
373
            // set release.nfostatus to failed
374
            Release::query()->where('id', $release['id'])->update(['nfostatus' => self::NFO_FAILED]);
375
        }
376
377
        if ($this->echo) {
378
            if ($nfoCount > 0) {
379
                echo PHP_EOL;
380
            }
381
            if ($ret > 0) {
382
                ColorCLI::doEcho($ret.' NFO file(s) found/processed.', true);
383
            }
384
        }
385
386
        return $ret;
387
    }
388
389
    /**
390
     * Get a string like this:
391
     * "AND r.nzbstatus = 1 AND r.nfostatus BETWEEN -8 AND -1 AND r.size < 1073741824 AND r.size > 1048576"
392
     * To use in a query.
393
     *
394
     * @return string
395
     * @throws \Exception
396
     * @static
397
     */
398
    public static function NfoQueryString(): string
399
    {
400
        $maxSize = (int) Settings::settingValue('..maxsizetoprocessnfo');
0 ignored issues
show
Bug introduced by
'..maxsizetoprocessnfo' of type string is incompatible with the type boolean|array expected by parameter $setting of App\Models\Settings::settingValue(). ( Ignorable by Annotation )

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

400
        $maxSize = (int) Settings::settingValue(/** @scrutinizer ignore-type */ '..maxsizetoprocessnfo');
Loading history...
401
        $minSize = (int) Settings::settingValue('..minsizetoprocessnfo');
402
        $dummy = (int) Settings::settingValue('..maxnforetries');
403
        $maxRetries = ($dummy >= 0 ? -($dummy + 1) : self::NFO_UNPROC);
404
405
        return sprintf(
406
            'AND r.nzbstatus = %d AND r.nfostatus BETWEEN %d AND %d %s %s',
407
            NZB::NZB_ADDED,
408
            ($maxRetries < -8 ? -8 : $maxRetries),
409
            self::NFO_UNPROC,
410
            ($maxSize > 0 ? ('AND r.size < '.($maxSize * 1073741824)) : ''),
411
            ($minSize > 0 ? ('AND r.size > '.($minSize * 1048576)) : '')
412
        );
413
    }
414
}
415