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

ProcessReleases::processIncompleteCollections()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 37
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 27
nc 8
nop 1
dl 0
loc 37
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace Blacklight\processing;
4
5
use Blacklight\NZB;
6
use Blacklight\NNTP;
7
use App\Models\Group;
8
use App\Models\Predb;
9
use Blacklight\db\DB;
10
use Blacklight\Genres;
11
use App\Models\Release;
12
use App\Models\Category;
13
use App\Models\Settings;
14
use Blacklight\ColorCLI;
15
use Blacklight\Releases;
16
use Blacklight\Categorize;
17
use App\Models\ReleaseRegex;
18
use Blacklight\ConsoleTools;
19
use Blacklight\ReleaseImage;
20
use App\Models\ReleasesGroups;
21
use Blacklight\ReleaseCleaning;
22
use App\Models\MultigroupPoster;
23
24
class ProcessReleases
25
{
26
    public const COLLFC_DEFAULT = 0; // Collection has default filecheck status
27
    public const COLLFC_COMPCOLL = 1; // Collection is a complete collection
28
    public const COLLFC_COMPPART = 2; // Collection is a complete collection and has all parts available
29
    public const COLLFC_SIZED = 3; // Collection has been calculated for total size
30
    public const COLLFC_INSERTED = 4; // Collection has been inserted into releases
31
    public const COLLFC_DELETE = 5; // Collection is ready for deletion
32
    public const COLLFC_TEMPCOMP = 15; // Collection is complete and being checked for complete parts
33
    public const COLLFC_ZEROPART = 16; // Collection has a 00/0XX designator (temporary)
34
35
    public const FILE_INCOMPLETE = 0; // We don't have all the parts yet for the file (binaries table partcheck column).
36
    public const FILE_COMPLETE = 1; // We have all the parts for the file (binaries table partcheck column).
37
38
    /**
39
     * @var int
40
     */
41
    public $collectionDelayTime;
42
43
    /**
44
     * @var int
45
     */
46
    public $crossPostTime;
47
48
    /**
49
     * @var int
50
     */
51
    public $releaseCreationLimit;
52
53
    /**
54
     * @var int
55
     */
56
    public $completion;
57
58
    /**
59
     * @var bool
60
     */
61
    public $echoCLI;
62
63
    /**
64
     * @var \Blacklight\db\DB
65
     */
66
    public $pdo;
67
68
    /**
69
     * @var \Blacklight\ConsoleTools
70
     */
71
    public $consoleTools;
72
73
    /**
74
     * @var \Blacklight\NZB
75
     */
76
    public $nzb;
77
78
    /**
79
     * @var \Blacklight\ReleaseCleaning
80
     */
81
    public $releaseCleaning;
82
83
    /**
84
     * @var \Blacklight\Releases
85
     */
86
    public $releases;
87
88
    /**
89
     * @var \Blacklight\ReleaseImage
90
     */
91
    public $releaseImage;
92
93
    /**
94
     * List of table names to be using for method calls.
95
     *
96
     *
97
     * @var array
98
     */
99
    protected $tables = [];
100
101
    /**
102
     * @var string
103
     */
104
    protected $fromNamesQuery;
105
106
    /**
107
     * Time (hours) to wait before delete a stuck/broken collection.
108
     *
109
     *
110
     * @var int
111
     */
112
    private $collectionTimeout;
113
114
    /**
115
     * @param array $options Class instances / Echo to cli ?
116
     *
117
     * @throws \Exception
118
     */
119
    public function __construct(array $options = [])
120
    {
121
        $defaults = [
122
            'Echo'            => true,
123
            'ConsoleTools'    => null,
124
            'Groups'          => null,
125
            'NZB'             => null,
126
            'ReleaseCleaning' => null,
127
            'ReleaseImage'    => null,
128
            'Releases'        => null,
129
            'Settings'        => null,
130
        ];
131
        $options += $defaults;
132
133
        $this->echoCLI = ($options['Echo'] && config('nntmux.echocli'));
134
135
        $this->pdo = ($options['Settings'] instanceof DB ? $options['Settings'] : new DB());
136
        $this->consoleTools = ($options['ConsoleTools'] instanceof ConsoleTools ? $options['ConsoleTools'] : new ConsoleTools());
137
        $this->nzb = ($options['NZB'] instanceof NZB ? $options['NZB'] : new NZB());
138
        $this->releaseCleaning = ($options['ReleaseCleaning'] instanceof ReleaseCleaning ? $options['ReleaseCleaning'] : new ReleaseCleaning($this->pdo));
139
        $this->releases = ($options['Releases'] instanceof Releases ? $options['Releases'] : new Releases(['Settings' => $this->pdo, 'Groups' => null]));
140
        $this->releaseImage = ($options['ReleaseImage'] instanceof ReleaseImage ? $options['ReleaseImage'] : new ReleaseImage());
141
142
        $dummy = Settings::settingValue('..delaytime');
0 ignored issues
show
Bug introduced by
'..delaytime' 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

142
        $dummy = Settings::settingValue(/** @scrutinizer ignore-type */ '..delaytime');
Loading history...
143
        $this->collectionDelayTime = ($dummy !== '' ? (int) $dummy : 2);
144
        $dummy = Settings::settingValue('..crossposttime');
145
        $this->crossPostTime = ($dummy !== '' ? (int) $dummy : 2);
146
        $dummy = Settings::settingValue('..maxnzbsprocessed');
147
        $this->releaseCreationLimit = ($dummy !== '' ? (int) $dummy : 1000);
148
        $dummy = Settings::settingValue('..completionpercent');
149
        $this->completion = ($dummy !== '' ? (int) $dummy : 0);
150
        if ($this->completion > 100) {
151
            $this->completion = 100;
152
            echo ColorCLI::error(PHP_EOL.'You have an invalid setting for completion. It cannot be higher than 100.');
153
        }
154
        $this->collectionTimeout = (int) Settings::settingValue('indexer.processing.collection_timeout');
155
    }
156
157
    /**
158
     * Main method for creating releases/NZB files from collections.
159
     *
160
     * @param int          $categorize
161
     * @param int          $postProcess
162
     * @param string       $groupName (optional)
163
     * @param \Blacklight\NNTP $nntp
164
     * @param bool         $echooutput
165
     *
166
     * @return int
167
     * @throws \Exception
168
     */
169
    public function processReleases($categorize, $postProcess, $groupName, &$nntp, $echooutput): int
170
    {
171
        $this->echoCLI = ($echooutput && config('nntmux.echocli'));
172
        $groupID = '';
173
174
        if (! empty($groupName) && $groupName !== 'mgr') {
175
            $groupInfo = Group::getByName($groupName);
176
            if ($groupInfo !== null) {
177
                $groupID = $groupInfo['id'];
178
            }
179
        }
180
181
        if ($this->echoCLI) {
182
            ColorCLI::doEcho(ColorCLI::header('Starting release update process ('.date('Y-m-d H:i:s').')'), true);
183
        }
184
185
        if (! file_exists(Settings::settingValue('..nzbpath'))) {
0 ignored issues
show
Bug introduced by
'..nzbpath' 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

185
        if (! file_exists(Settings::settingValue(/** @scrutinizer ignore-type */ '..nzbpath'))) {
Loading history...
186
            if ($this->echoCLI) {
187
                ColorCLI::doEcho(
188
                    ColorCLI::error('Bad or missing nzb directory - '.Settings::settingValue('..nzbpath')),
189
                    true
190
                );
191
            }
192
193
            return 0;
194
        }
195
196
        $this->processIncompleteCollections($groupID);
197
        $this->processCollectionSizes($groupID);
198
        $this->deleteUnwantedCollections($groupID);
199
200
        $totalReleasesAdded = 0;
201
        do {
202
            $releasesCount = $this->createReleases($groupID);
203
            $totalReleasesAdded += $releasesCount['added'];
204
205
            $nzbFilesAdded = $this->createNZBs($groupID);
206
207
            $this->categorizeReleases($categorize, $groupID);
208
            $this->postProcessReleases($postProcess, $nntp);
209
            $this->deleteCollections($groupID);
210
211
            // This loops as long as the number of releases or nzbs added was >= the limit (meaning there are more waiting to be created)
212
        } while (
213
            ($releasesCount['added'] + $releasesCount['dupes']) >= $this->releaseCreationLimit
214
            || $nzbFilesAdded >= $this->releaseCreationLimit
215
        );
216
217
        // Only run if non-mgr as mgr is not specific to group
218
        if ($groupName !== 'mgr') {
219
            $this->deletedReleasesByGroup($groupID);
220
            $this->deleteReleases();
221
        }
222
223
        return $totalReleasesAdded;
224
    }
225
226
    /**
227
     * Return all releases to other->misc category.
228
     *
229
     * @param string $where Optional "where" query parameter.
230
     *
231
     * @void
232
     */
233
    public function resetCategorize($where = ''): void
234
    {
235
        $this->pdo->queryExec(
236
            sprintf('UPDATE releases SET categories_id = %d, iscategorized = 0 %s', Category::OTHER_MISC, $where)
237
        );
238
    }
239
240
    /**
241
     * Categorizes releases.
242
     *
243
     * @param string $type  name or searchname | Categorize using the search name or subject.
244
     * @param string $where Optional "where" query parameter.
245
     *
246
     * @return int Quantity of categorized releases.
247
     * @throws \Exception
248
     */
249
    public function categorizeRelease($type, $where = ''): int
250
    {
251
        $cat = new Categorize(['Settings' => $this->pdo]);
252
        $categorized = $total = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $total is dead and can be removed.
Loading history...
253
        $releases = $this->pdo->queryDirect(
254
            sprintf(
255
                '
256
				SELECT id, fromname, %s, groups_id
257
				FROM releases %s',
258
                $type,
259
                $where
260
            )
261
        );
262
        if ($releases && $releases->rowCount()) {
263
            $total = $releases->rowCount();
264
            foreach ($releases as $release) {
265
                $catId = $cat->determineCategory($release['groups_id'], $release[$type], $release['fromname']);
266
                Release::query()->where('id', $release['id'])->update(['categories_id' => $catId, 'iscategorized' => 1]);
267
                $categorized++;
268
                if ($this->echoCLI) {
269
                    $this->consoleTools->overWritePrimary(
270
                        'Categorizing: '.$this->consoleTools->percentString($categorized, $total)
271
                    );
272
                }
273
            }
274
        }
275
        if ($this->echoCLI !== false && $categorized > 0) {
276
            echo PHP_EOL;
277
        }
278
279
        return $categorized;
280
    }
281
282
    /**
283
     * @param $groupID
284
     * @throws \Exception
285
     */
286
    public function processIncompleteCollections($groupID): void
287
    {
288
        $startTime = time();
289
        $this->initiateTableNames($groupID);
290
291
        if ($this->echoCLI) {
292
            ColorCLI::doEcho(ColorCLI::header('Process Releases -> Attempting to find complete collections.'));
293
        }
294
295
        $where = (! empty($groupID) ? ' AND c.groups_id = '.$groupID.' ' : ' ');
296
297
        $this->processStuckCollections($where);
298
        $this->collectionFileCheckStage1($where);
299
        $this->collectionFileCheckStage2($where);
300
        $this->collectionFileCheckStage3($where);
301
        $this->collectionFileCheckStage4($where);
302
        $this->collectionFileCheckStage5($where);
303
        $this->collectionFileCheckStage6($where);
304
305
        if ($this->echoCLI) {
306
            $count = $this->pdo->queryOneRow(
307
                sprintf(
308
                    '
309
					SELECT COUNT(c.id) AS complete
310
					FROM %s c
311
					WHERE c.filecheck = %d %s',
312
                    $this->tables['cname'],
313
                    self::COLLFC_COMPPART,
314
                    $where
315
                )
316
            );
317
            ColorCLI::doEcho(
318
                ColorCLI::primary(
319
                    ($count === false ? 0 : $count['complete']).' collections were found to be complete. Time: '.
320
                    $this->consoleTools->convertTime(time() - $startTime)
321
                ),
322
                true
323
            );
324
        }
325
    }
326
327
    /**
328
     * @param $groupID
329
     */
330
    public function processCollectionSizes($groupID): void
331
    {
332
        $startTime = time();
333
        $this->initiateTableNames($groupID);
334
335
        if ($this->echoCLI) {
336
            ColorCLI::doEcho(ColorCLI::header('Process Releases -> Calculating collection sizes (in bytes).'), true);
337
        }
338
        // Get the total size in bytes of the collection for collections where filecheck = 2.
339
        $checked = $this->pdo->queryExec(
340
            sprintf(
341
                '
342
				UPDATE %s c
343
				SET c.filesize =
344
				(
345
					SELECT COALESCE(SUM(b.partsize), 0)
346
					FROM %s b
347
					WHERE b.collections_id = c.id
348
				),
349
				c.filecheck = %d
350
				WHERE c.filecheck = %d
351
				AND c.filesize = 0 %s',
352
                $this->tables['cname'],
353
                $this->tables['bname'],
354
                self::COLLFC_SIZED,
355
                self::COLLFC_COMPPART,
356
                (! empty($groupID) ? ' AND c.groups_id = '.$groupID : ' ')
357
            )
358
        );
359
        if ($checked !== false && $this->echoCLI) {
360
            ColorCLI::doEcho(
361
                ColorCLI::primary(
362
                    $checked->rowCount().' collections set to filecheck = 3(size calculated)'
363
                ), true
364
            );
365
            ColorCLI::doEcho(ColorCLI::primary($this->consoleTools->convertTime(time() - $startTime)), true);
366
        }
367
    }
368
369
    /**
370
     * @param $groupID
371
     *
372
     * @throws \Exception
373
     */
374
    public function deleteUnwantedCollections($groupID): void
375
    {
376
        $startTime = time();
377
        $this->initiateTableNames($groupID);
378
379
        if ($this->echoCLI) {
380
            ColorCLI::doEcho(
381
                ColorCLI::header(
382
                    'Process Releases -> Delete collections smaller/larger than minimum size/file count from group/site setting.'
383
                ), true
384
            );
385
        }
386
387
        $groupID === '' ? $groupIDs = Group::getActiveIDs() : $groupIDs = [['id' => $groupID]];
388
389
        $minSizeDeleted = $maxSizeDeleted = $minFilesDeleted = 0;
390
391
        $maxSizeSetting = Settings::settingValue('.release.maxsizetoformrelease');
392
        $minSizeSetting = Settings::settingValue('.release.minsizetoformrelease');
393
        $minFilesSetting = Settings::settingValue('.release.minfilestoformrelease');
394
395
        foreach ($groupIDs as $grpID) {
396
            $groupMinSizeSetting = $groupMinFilesSetting = 0;
397
398
            $groupMinimums = Group::getGroupByID($grpID['id']);
399
            if ($groupMinimums !== null) {
400
                if (! empty($groupMinimums['minsizetoformrelease']) && $groupMinimums['minsizetoformrelease'] > 0) {
401
                    $groupMinSizeSetting = (int) $groupMinimums['minsizetoformrelease'];
402
                }
403
                if (! empty($groupMinimums['minfilestoformrelease']) && $groupMinimums['minfilestoformrelease'] > 0) {
404
                    $groupMinFilesSetting = (int) $groupMinimums['minfilestoformrelease'];
405
                }
406
            }
407
408
            if ($this->pdo->queryOneRow(
409
                    sprintf(
410
                        '
411
						SELECT SQL_NO_CACHE id
412
						FROM %s c
413
						WHERE c.filecheck = %d
414
						AND c.filesize > 0',
415
                        $this->tables['cname'],
416
                        self::COLLFC_SIZED
417
                    )
418
                ) !== false
419
            ) {
420
                $deleteQuery = $this->pdo->queryExec(
421
                    sprintf(
422
                        '
423
						DELETE c FROM %s c
424
						WHERE c.filecheck = %d
425
						AND c.filesize > 0
426
						AND GREATEST(%d, %d) > 0
427
						AND c.filesize < GREATEST(%d, %d)',
428
                        $this->tables['cname'],
429
                        self::COLLFC_SIZED,
430
                        $groupMinSizeSetting,
431
                        $minSizeSetting,
432
                        $groupMinSizeSetting,
433
                        $minSizeSetting
434
                    )
435
                );
436
                if ($deleteQuery !== false) {
437
                    $minSizeDeleted += $deleteQuery->rowCount();
438
                }
439
440
                if ($maxSizeSetting > 0) {
441
                    $deleteQuery = $this->pdo->queryExec(
442
                        sprintf(
443
                            '
444
							DELETE c FROM %s c
445
							WHERE c.filecheck = %d
446
							AND c.filesize > %d',
447
                            $this->tables['cname'],
448
                            self::COLLFC_SIZED,
449
                            $maxSizeSetting
450
                        )
451
                    );
452
                    if ($deleteQuery !== false) {
453
                        $maxSizeDeleted += $deleteQuery->rowCount();
454
                    }
455
                }
456
457
                if ($minFilesSetting > 0 || $groupMinFilesSetting > 0) {
458
                    $deleteQuery = $this->pdo->queryExec(
459
                        sprintf(
460
                            '
461
						DELETE c FROM %s c
462
						WHERE c.filecheck = %d
463
						AND GREATEST(%d, %d) > 0
464
						AND c.totalfiles < GREATEST(%d, %d)',
465
                            $this->tables['cname'],
466
                            self::COLLFC_SIZED,
467
                            $groupMinFilesSetting,
468
                            $minFilesSetting,
469
                            $groupMinFilesSetting,
470
                            $minFilesSetting
471
                        )
472
                    );
473
                    if ($deleteQuery !== false) {
474
                        $minFilesDeleted += $deleteQuery->rowCount();
475
                    }
476
                }
477
            }
478
        }
479
480
        if ($this->echoCLI) {
481
            ColorCLI::doEcho(
482
                ColorCLI::primary(
483
                    'Deleted '.($minSizeDeleted + $maxSizeDeleted + $minFilesDeleted).' collections: '.PHP_EOL.
484
                    $minSizeDeleted.' smaller than, '.
485
                    $maxSizeDeleted.' bigger than, '.
486
                    $minFilesDeleted.' with less files than site/group settings in: '.
487
                    $this->consoleTools->convertTime(time() - $startTime)
488
                ),
489
                true
490
            );
491
        }
492
    }
493
494
    /**
495
     * @param $groupID
496
     * @throws \Exception
497
     */
498
    protected function initiateTableNames($groupID): void
499
    {
500
        $this->tables = (new Group())->getCBPTableNames($groupID);
501
    }
502
503
    /**
504
     * Form fromNamesQuery for creating NZBs.
505
     *
506
     * @void
507
     */
508
    protected function formFromNamesQuery(): void
509
    {
510
        $posters = MultigroupPoster::commaSeparatedList();
511
        $this->fromNamesQuery = sprintf("AND r.fromname NOT IN('%s')", $posters);
512
    }
513
514
    /**
515
     * @param int|string $groupID (optional)
516
     *
517
     * @return array
518
     * @throws \Exception
519
     */
520
    public function createReleases($groupID): array
521
    {
522
        $startTime = time();
523
        $this->initiateTableNames($groupID);
524
525
        $categorize = new Categorize(['Settings' => $this->pdo]);
526
        $returnCount = $duplicate = 0;
527
528
        if ($this->echoCLI) {
529
            ColorCLI::doEcho(ColorCLI::header('Process Releases -> Create releases from complete collections.'), true);
530
        }
531
532
        $this->pdo->ping(true);
533
534
        $collections = $this->pdo->queryDirect(
535
            sprintf(
536
                '
537
				SELECT SQL_NO_CACHE c.*, g.name AS gname
538
				FROM %s c
539
				INNER JOIN groups g ON c.groups_id = g.id
540
				WHERE %s c.filecheck = %d
541
				AND c.filesize > 0
542
				LIMIT %d',
543
                $this->tables['cname'],
544
                (! empty($groupID) ? ' c.groups_id = '.$groupID.' AND ' : ' '),
545
                self::COLLFC_SIZED,
546
                $this->releaseCreationLimit
547
            )
548
        );
549
550
        if ($this->echoCLI && $collections !== false) {
551
            echo ColorCLI::primary($collections->rowCount().' Collections ready to be converted to releases.');
552
        }
553
554
        if ($collections instanceof \Traversable) {
555
            foreach ($collections as $collection) {
556
                $cleanRelName = utf8_encode(str_replace(['#', '@', '$', '%', '^', '§', '¨', '©', 'Ö'], '', $collection['subject']));
557
                $fromName = utf8_encode(
558
                    trim($collection['fromname'], "'")
559
                );
560
561
                // Look for duplicates, duplicates match on releases.name, releases.fromname and releases.size
562
                // A 1% variance in size is considered the same size when the subject and poster are the same
563
                $dupeCheck = Release::query()
564
                    ->where(['name' => $cleanRelName, 'fromname' => $fromName])
565
                    ->whereBetween('size', [$collection['filesize'] * .99, $collection['filesize'] * 1.01])
566
                    ->first(['id']);
567
568
                if ($dupeCheck === null) {
569
                    $cleanedName = $this->releaseCleaning->releaseCleaner(
570
                        $collection['subject'],
571
                        $collection['fromname'],
572
                        $collection['filesize'],
573
                        $collection['gname']
574
                    );
575
576
                    if (\is_array($cleanedName)) {
577
                        $properName = $cleanedName['properlynamed'];
578
                        $preID = $cleanerName['predb'] ?? false;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cleanerName does not exist. Did you maybe mean $cleanedName?
Loading history...
579
                        $cleanedName = $cleanedName['cleansubject'];
580
                    } else {
581
                        $properName = true;
582
                        $preID = false;
583
                    }
584
585
                    if ($preID === false && $cleanedName !== '') {
586
                        // try to match the cleaned searchname to predb title or filename here
587
                        $preMatch = Predb::matchPre($cleanedName);
588
                        if ($preMatch !== false) {
589
                            $cleanedName = $preMatch['title'];
590
                            $preID = $preMatch['predb_id'];
591
                            $properName = true;
592
                        }
593
                    }
594
595
                    $releaseID = Release::insertRelease(
596
                        [
597
                            'name' => $cleanRelName,
598
                            'searchname' => utf8_encode($cleanedName),
599
                            'totalpart' => $collection['totalfiles'],
600
                            'groups_id' => $collection['groups_id'],
601
                            'guid' => createGUID(),
602
                            'postdate' => $collection['date'],
603
                            'fromname' => $fromName,
604
                            'size' => $collection['filesize'],
605
                            'categories_id' => $categorize->determineCategory($collection['groups_id'], $cleanedName),
606
                            'isrenamed' => $properName === true ? 1 : 0,
607
                            'predb_id' => $preID === false ? 0 : $preID,
608
                            'nzbstatus' => NZB::NZB_NONE,
609
                        ]
610
                    );
611
612
                    if ($releaseID !== false) {
613
                        // Update collections table to say we inserted the release.
614
                        $this->pdo->queryExec(
615
                            sprintf(
616
                                '
617
								UPDATE %s
618
								SET filecheck = %d, releases_id = %d
619
								WHERE id = %d',
620
                                $this->tables['cname'],
621
                                self::COLLFC_INSERTED,
622
                                $releaseID,
623
                                $collection['id']
624
                            )
625
                        );
626
627
                        // Add the id of regex that matched the collection and release name to release_regexes table
628
                        ReleaseRegex::insertIgnore([
629
                            'releases_id'            => $releaseID,
630
                            'collection_regex_id'    => $collection['collection_regexes_id'],
631
                            'naming_regex_id'        => $cleanedName['id'] ?? 0,
632
                        ]);
633
634
                        if (preg_match_all('#(\S+):\S+#', $collection['xref'], $matches)) {
635
                            foreach ($matches[1] as $grp) {
636
                                //check if the group name is in a valid format
637
                                $grpTmp = Group::isValidGroup($grp);
638
                                if ($grpTmp !== false) {
639
                                    //check if the group already exists in database
640
                                    $xrefGrpID = Group::getIDByName($grpTmp);
641
                                    if ($xrefGrpID === '') {
642
                                        $xrefGrpID = Group::addGroup(
643
                                            [
644
                                                'name'                  => $grpTmp,
645
                                                'description'           => 'Added by Release processing',
646
                                                'backfill_target'       => 1,
647
                                                'first_record'          => 0,
648
                                                'last_record'           => 0,
649
                                                'active'                => 0,
650
                                                'backfill'              => 0,
651
                                                'minfilestoformrelease' => '',
652
                                                'minsizetoformrelease'  => '',
653
                                            ]
654
                                        );
655
                                    }
656
657
                                    $relGroupsChk = ReleasesGroups::query()->where(
658
                                        [
659
                                            ['releases_id', '=', $releaseID],
660
                                            ['groups_id', '=', $xrefGrpID],
661
                                        ]
662
                                    )->first();
663
664
                                    if ($relGroupsChk === null) {
665
                                        ReleasesGroups::query()->insert(
666
                                            [
667
                                                'releases_id' => $releaseID,
668
                                                'groups_id'   => $xrefGrpID,
669
                                            ]
670
                                        );
671
                                    }
672
                                }
673
                            }
674
                        }
675
676
                        $returnCount++;
677
678
                        if ($this->echoCLI) {
679
                            echo "Added $returnCount releases.\r";
680
                        }
681
                    }
682
                } else {
683
                    // The release was already in the DB, so delete the collection.
684
                    $this->pdo->queryExec(
685
                        sprintf(
686
                            '
687
							DELETE c
688
							FROM %s c
689
							WHERE c.collectionhash = %s',
690
                            $this->tables['cname'],
691
                            $this->pdo->escapeString($collection['collectionhash'])
692
                        )
693
                    );
694
                    $duplicate++;
695
                }
696
            }
697
        }
698
699
        if ($this->echoCLI) {
700
            ColorCLI::doEcho(
701
                ColorCLI::primary(
702
                    PHP_EOL.
703
                    number_format($returnCount).
704
                    ' Releases added and '.
705
                    number_format($duplicate).
706
                    ' duplicate collections deleted in '.
707
                    $this->consoleTools->convertTime(time() - $startTime)
708
                ),
709
                true
710
            );
711
        }
712
713
        return ['added' => $returnCount, 'dupes' => $duplicate];
714
    }
715
716
    /**
717
     * Create NZB files from complete releases.
718
     *
719
     * @param int|string $groupID (optional)
720
     *
721
     * @return int
722
     * @throws \RuntimeException
723
     */
724
    public function createNZBs($groupID): int
725
    {
726
        $startTime = time();
727
        $this->formFromNamesQuery();
728
729
        if ($this->echoCLI) {
730
            ColorCLI::doEcho(ColorCLI::header('Process Releases -> Create the NZB, delete collections/binaries/parts.'), true);
731
        }
732
733
        $releases = $this->pdo->queryDirect(
734
            sprintf(
735
                "
736
				SELECT SQL_NO_CACHE
737
					CONCAT(COALESCE(cp.title,'') , CASE WHEN cp.title IS NULL THEN '' ELSE ' > ' END , c.title) AS title,
738
					r.name, r.id, r.guid
739
				FROM releases r
740
				INNER JOIN categories c ON r.categories_id = c.id
741
				INNER JOIN categories cp ON cp.id = c.parentid
742
				WHERE %s nzbstatus = 0 %s",
743
                (! empty($groupID) ? ' r.groups_id = '.$groupID.' AND ' : ' '),
744
                $this->fromNamesQuery
745
            )
746
        );
747
748
        $nzbCount = 0;
749
750
        if ($releases && $releases->rowCount()) {
751
            $total = $releases->rowCount();
752
            // Init vars for writing the NZB's.
753
            $this->nzb->initiateForWrite($groupID);
0 ignored issues
show
Bug introduced by
It seems like $groupID can also be of type string; however, parameter $groupID of Blacklight\NZB::initiateForWrite() does only seem to accept integer, 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

753
            $this->nzb->initiateForWrite(/** @scrutinizer ignore-type */ $groupID);
Loading history...
754
            foreach ($releases as $release) {
755
                if ($this->nzb->writeNZBforReleaseId($release['id'], $release['guid'], $release['name'], $release['title']) === true) {
756
                    $nzbCount++;
757
                    if ($this->echoCLI) {
758
                        echo ColorCLI::primaryOver("Creating NZBs and deleting Collections:\t".$nzbCount.'/'.$total."\r");
759
                    }
760
                }
761
            }
762
        }
763
764
        $totalTime = (time() - $startTime);
765
766
        if ($this->echoCLI) {
767
            ColorCLI::doEcho(
768
                ColorCLI::primary(
769
                    number_format($nzbCount).' NZBs created/Collections deleted in '.
770
                    $totalTime.' seconds.'.PHP_EOL.
771
                    'Total time: '.ColorCLI::primary($this->consoleTools->convertTime($totalTime)).PHP_EOL
772
                ), true
773
            );
774
        }
775
776
        return $nzbCount;
777
    }
778
779
    /**
780
     * Categorize releases.
781
     *
782
     * @param int        $categorize
783
     * @param int|string $groupID (optional)
784
     *
785
     * @void
786
     * @throws \Exception
787
     */
788
    public function categorizeReleases($categorize, $groupID = ''): void
789
    {
790
        $startTime = time();
791
        if ($this->echoCLI) {
792
            echo ColorCLI::header('Process Releases -> Categorize releases.');
793
        }
794
        switch ((int) $categorize) {
795
            case 2:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
796
                $type = 'searchname';
797
                break;
798
            case 1:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
799
            default:
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

switch ($expr) {
    default:
        doSomething(); //right
        break;
}


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
800
801
                $type = 'name';
802
                break;
803
        }
804
        $this->categorizeRelease(
805
            $type,
806
            (! empty($groupID)
807
                ? 'WHERE categories_id = '.Category::OTHER_MISC.' AND iscategorized = 0 AND groups_id = '.$groupID
808
                : 'WHERE categories_id = '.Category::OTHER_MISC.' AND iscategorized = 0')
809
        );
810
811
        if ($this->echoCLI) {
812
            ColorCLI::doEcho(ColorCLI::primary($this->consoleTools->convertTime(time() - $startTime)), true);
813
        }
814
    }
815
816
    /**
817
     * Post-process releases.
818
     *
819
     * @param int  $postProcess
820
     * @param NNTP $nntp
821
     *
822
     * @void
823
     * @throws \Exception
824
     */
825
    public function postProcessReleases($postProcess, &$nntp): void
826
    {
827
        if ((int) $postProcess === 1) {
828
            (new PostProcess(['Echo' => $this->echoCLI, 'Settings' => $this->pdo, 'Groups' => $this->groups]))->processAll($nntp);
0 ignored issues
show
Bug Best Practice introduced by
The property groups does not exist on Blacklight\processing\ProcessReleases. Did you maybe forget to declare it?
Loading history...
829
        } else {
830
            if ($this->echoCLI) {
831
                ColorCLI::doEcho(
832
                    ColorCLI::info(
833
                        "\nPost-processing is not running inside the Process Releases class.\n".
834
                        'If you are using tmux or screen they might have their own scripts running Post-processing.'
835
                    ), true
836
                );
837
            }
838
        }
839
    }
840
841
    /**
842
     * @param $groupID
843
     *
844
     * @throws \Exception
845
     */
846
    public function deleteCollections($groupID): void
847
    {
848
        $startTime = time();
849
        $this->initiateTableNames($groupID);
850
851
        $deletedCount = 0;
852
853
        // CBP older than retention.
854
        if ($this->echoCLI) {
855
            echo
856
                ColorCLI::header('Process Releases -> Delete finished collections.'.PHP_EOL).
857
                ColorCLI::primary(sprintf(
858
                    'Deleting collections/binaries/parts older than %d hours.',
859
                    Settings::settingValue('..partretentionhours')
860
                ));
861
        }
862
863
        $deleted = 0;
864
        $deleteQuery = $this->pdo->queryExec(
865
            sprintf(
866
                '
867
				DELETE c
868
				FROM %s c
869
				WHERE (c.dateadded < NOW() - INTERVAL %d HOUR)',
870
                $this->tables['cname'],
871
                Settings::settingValue('..partretentionhours')
872
            )
873
        );
874
875
        if ($deleteQuery !== false) {
876
            $deleted = $deleteQuery->rowCount();
877
            $deletedCount += $deleted;
878
        }
879
880
        $firstQuery = $fourthQuery = time();
881
882
        if ($this->echoCLI) {
883
            echo ColorCLI::primary(
884
                'Finished deleting '.$deleted.' old collections/binaries/parts in '.
885
                ($firstQuery - $startTime).' seconds.'.PHP_EOL
886
            );
887
        }
888
889
        // Cleanup orphaned collections, binaries and parts
890
        // this really shouldn't happen, but just incase - so we only run 1/200 of the time
891
        if (random_int(0, 200) <= 1) {
892
            // CBP collection orphaned with no binaries or parts.
893
            if ($this->echoCLI) {
894
                echo
895
                    ColorCLI::header('Process Releases -> Remove CBP orphans.'.PHP_EOL).
896
                    ColorCLI::primary('Deleting orphaned collections.');
897
            }
898
899
            $deleted = 0;
900
            $deleteQuery = $this->pdo->queryExec(
901
                sprintf(
902
                    '
903
					DELETE c, b, p
904
					FROM %s c
905
					LEFT JOIN %s b ON c.id = b.collections_id
906
					LEFT JOIN %s p ON b.id = p.binaries_id
907
					WHERE (b.id IS NULL OR p.binaries_id IS NULL)',
908
                    $this->tables['cname'],
909
                    $this->tables['bname'],
910
                    $this->tables['pname']
911
                )
912
            );
913
914
            if ($deleteQuery !== false) {
915
                $deleted = $deleteQuery->rowCount();
916
                $deletedCount += $deleted;
917
            }
918
919
            $secondQuery = time();
920
921
            if ($this->echoCLI) {
922
                echo ColorCLI::primary(
923
                    'Finished deleting '.$deleted.' orphaned collections in '.
924
                    ($secondQuery - $firstQuery).' seconds.'.PHP_EOL
925
                );
926
            }
927
928
            // orphaned binaries - binaries with no parts or binaries with no collection
929
            // Don't delete currently inserting binaries by checking the max id.
930
            if ($this->echoCLI) {
931
                echo ColorCLI::primary('Deleting orphaned binaries/parts with no collection.');
932
            }
933
934
            $deleted = 0;
935
            $deleteQuery = $this->pdo->queryExec(
936
                sprintf(
937
                    'DELETE b, p FROM %s b
938
					LEFT JOIN %s p ON b.id = p.binaries_id
939
					LEFT JOIN %s c ON b.collections_id = c.id
940
					WHERE (p.binaries_id IS NULL OR c.id IS NULL)
941
					AND b.id < %d',
942
                    $this->tables['bname'],
943
                    $this->tables['pname'],
944
                    $this->tables['cname'],
945
                    $this->maxQueryFormulator($this->tables['bname'], 20000)
946
                )
947
            );
948
949
            if ($deleteQuery !== false) {
950
                $deleted = $deleteQuery->rowCount();
951
                $deletedCount += $deleted;
952
            }
953
954
            $thirdQuery = time();
955
956
            if ($this->echoCLI) {
957
                echo ColorCLI::primary(
958
                    'Finished deleting '.$deleted.' binaries with no collections or parts in '.
959
                    ($thirdQuery - $secondQuery).' seconds.'
960
                );
961
            }
962
963
            // orphaned parts - parts with no binary
964
            // Don't delete currently inserting parts by checking the max id.
965
            if ($this->echoCLI) {
966
                echo ColorCLI::primary('Deleting orphaned parts with no binaries.');
967
            }
968
            $deleted = 0;
969
            $deleteQuery = $this->pdo->queryExec(
970
                sprintf(
971
                    '
972
					DELETE p
973
					FROM %s p
974
					LEFT JOIN %s b ON p.binaries_id = b.id
975
					WHERE b.id IS NULL
976
					AND p.binaries_id < %d',
977
                    $this->tables['pname'],
978
                    $this->tables['bname'],
979
                    $this->maxQueryFormulator($this->tables['bname'], 20000)
980
                )
981
            );
982
            if ($deleteQuery !== false) {
983
                $deleted = $deleteQuery->rowCount();
984
                $deletedCount += $deleted;
985
            }
986
987
            $fourthQuery = time();
988
989
            if ($this->echoCLI) {
990
                echo ColorCLI::primary(
991
                    'Finished deleting '.$deleted.' parts with no binaries in '.
992
                    ($fourthQuery - $thirdQuery).' seconds.'.PHP_EOL
993
                );
994
            }
995
        } // done cleaning up Binaries/Parts orphans
996
997
        if ($this->echoCLI) {
998
            echo ColorCLI::primary(
999
                'Deleting collections that were missed after NZB creation.'
1000
            );
1001
        }
1002
1003
        $deleted = 0;
1004
        // Collections that were missing on NZB creation.
1005
        $collections = $this->pdo->queryDirect(
1006
            sprintf(
1007
                '
1008
				SELECT SQL_NO_CACHE c.id
1009
				FROM %s c
1010
				INNER JOIN releases r ON r.id = c.releases_id
1011
				WHERE r.nzbstatus = 1',
1012
                $this->tables['cname']
1013
            )
1014
        );
1015
1016
        if ($collections instanceof \Traversable) {
1017
            foreach ($collections as $collection) {
1018
                $deleted++;
1019
                $this->pdo->queryExec(
1020
                    sprintf(
1021
                        '
1022
						DELETE c
1023
						FROM %s c
1024
						WHERE c.id = %d',
1025
                        $this->tables['cname'],
1026
                        $collection['id']
1027
                    )
1028
                );
1029
            }
1030
            $deletedCount += $deleted;
1031
        }
1032
1033
        if ($this->echoCLI) {
1034
            ColorCLI::doEcho(
1035
                ColorCLI::primary(
1036
                    'Finished deleting '.$deleted.' collections missed after NZB creation in '.
1037
                    (time() - $fourthQuery).' seconds.'.PHP_EOL.
1038
                    'Removed '.
1039
                    number_format($deletedCount).
1040
                    ' parts/binaries/collection rows in '.
1041
                    $this->consoleTools->convertTime($fourthQuery - $startTime).PHP_EOL
1042
                ), true
1043
            );
1044
        }
1045
    }
1046
1047
    /**
1048
     * Delete unwanted releases based on admin settings.
1049
     * This deletes releases based on group.
1050
     *
1051
     * @param int|string $groupID (optional)
1052
     *
1053
     * @void
1054
     * @throws \Exception
1055
     */
1056
    public function deletedReleasesByGroup($groupID = ''): void
1057
    {
1058
        $startTime = time();
1059
        $minSizeDeleted = $maxSizeDeleted = $minFilesDeleted = 0;
1060
1061
        if ($this->echoCLI) {
1062
            echo ColorCLI::header('Process Releases -> Delete releases smaller/larger than minimum size/file count from group/site setting.');
1063
        }
1064
1065
        $groupID === '' ? $groupIDs = Group::getActiveIDs() : $groupIDs = [['id' => $groupID]];
1066
1067
        $maxSizeSetting = Settings::settingValue('.release.maxsizetoformrelease');
0 ignored issues
show
Bug introduced by
'.release.maxsizetoformrelease' 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

1067
        $maxSizeSetting = Settings::settingValue(/** @scrutinizer ignore-type */ '.release.maxsizetoformrelease');
Loading history...
1068
        $minSizeSetting = Settings::settingValue('.release.minsizetoformrelease');
1069
        $minFilesSetting = Settings::settingValue('.release.minfilestoformrelease');
1070
1071
        foreach ($groupIDs as $grpID) {
1072
            $releases = $this->pdo->queryDirect(
1073
                sprintf(
1074
                    '
1075
					SELECT SQL_NO_CACHE r.guid, r.id
1076
					FROM releases r
1077
					INNER JOIN groups g ON g.id = r.groups_id
1078
					WHERE r.groups_id = %d
1079
					AND greatest(IFNULL(g.minsizetoformrelease, 0), %d) > 0
1080
					AND r.size < greatest(IFNULL(g.minsizetoformrelease, 0), %d)',
1081
                    $grpID['id'],
1082
                    $minSizeSetting,
1083
                    $minSizeSetting
1084
                )
1085
            );
1086
            if ($releases instanceof \Traversable) {
1087
                foreach ($releases as $release) {
1088
                    $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1089
                    $minSizeDeleted++;
1090
                }
1091
            }
1092
1093
            if ($maxSizeSetting > 0) {
1094
                $releases = $this->pdo->queryDirect(
1095
                    sprintf(
1096
                        '
1097
						SELECT SQL_NO_CACHE id, guid
1098
						FROM releases
1099
						WHERE groups_id = %d
1100
						AND size > %d',
1101
                        $grpID['id'],
1102
                        $maxSizeSetting
1103
                    )
1104
                );
1105
                if ($releases instanceof \Traversable) {
1106
                    foreach ($releases as $release) {
1107
                        $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1108
                        $maxSizeDeleted++;
1109
                    }
1110
                }
1111
            }
1112
            if ($minFilesSetting > 0) {
1113
                $releases = $this->pdo->queryDirect(
1114
                     sprintf(
1115
                         '
1116
				SELECT SQL_NO_CACHE r.id, r.guid
1117
				FROM releases r
1118
				INNER JOIN groups g ON g.id = r.groups_id
1119
				WHERE r.groups_id = %d
1120
				AND greatest(IFNULL(g.minfilestoformrelease, 0), %d) > 0
1121
				AND r.totalpart < greatest(IFNULL(g.minfilestoformrelease, 0), %d)',
1122
                         $grpID['id'],
1123
                         $minFilesSetting,
1124
                         $minFilesSetting
1125
                     )
1126
                 );
1127
                if ($releases instanceof \Traversable) {
1128
                    foreach ($releases as $release) {
1129
                        $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1130
                        $minFilesDeleted++;
1131
                    }
1132
                }
1133
            }
1134
        }
1135
1136
        if ($this->echoCLI) {
1137
            ColorCLI::doEcho(
1138
                ColorCLI::primary(
1139
                    'Deleted '.($minSizeDeleted + $maxSizeDeleted + $minFilesDeleted).
1140
                    ' releases: '.PHP_EOL.
1141
                    $minSizeDeleted.' smaller than, '.$maxSizeDeleted.' bigger than, '.$minFilesDeleted.
1142
                    ' with less files than site/groups setting in: '.
1143
                    $this->consoleTools->convertTime(time() - $startTime)
1144
                ),
1145
                true
1146
            );
1147
        }
1148
    }
1149
1150
    /**
1151
     * Delete releases using admin settings.
1152
     * This deletes releases, regardless of group.
1153
     *
1154
     * @void
1155
     * @throws \Exception
1156
     */
1157
    public function deleteReleases(): void
1158
    {
1159
        $startTime = time();
1160
        $genres = new Genres(['Settings' => $this->pdo]);
1161
        $passwordDeleted = $duplicateDeleted = $retentionDeleted = $completionDeleted = $disabledCategoryDeleted = 0;
1162
        $disabledGenreDeleted = $miscRetentionDeleted = $miscHashedDeleted = $categoryMinSizeDeleted = 0;
1163
1164
        // Delete old releases and finished collections.
1165
        if ($this->echoCLI) {
1166
            ColorCLI::doEcho(ColorCLI::header('Process Releases -> Delete old releases and passworded releases.'), true);
1167
        }
1168
1169
        // Releases past retention.
1170
        if ((int) Settings::settingValue('..releaseretentiondays') !== 0) {
0 ignored issues
show
Bug introduced by
'..releaseretentiondays' 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

1170
        if ((int) Settings::settingValue(/** @scrutinizer ignore-type */ '..releaseretentiondays') !== 0) {
Loading history...
1171
            $releases = $this->pdo->queryDirect(
1172
                sprintf(
1173
                    'SELECT SQL_NO_CACHE id, guid FROM releases WHERE postdate < (NOW() - INTERVAL %d DAY)',
1174
                    (int) Settings::settingValue('..releaseretentiondays')
1175
                )
1176
            );
1177
            if ($releases instanceof \Traversable) {
1178
                foreach ($releases as $release) {
1179
                    $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1180
                    $retentionDeleted++;
1181
                }
1182
            }
1183
        }
1184
1185
        // Passworded releases.
1186
        if ((int) Settings::settingValue('..deletepasswordedrelease') === 1) {
1187
            $releases = $this->pdo->queryDirect(
1188
                sprintf(
1189
                    'SELECT SQL_NO_CACHE id, guid FROM releases WHERE passwordstatus = %d',
1190
                    Releases::PASSWD_RAR
1191
                )
1192
            );
1193
            if ($releases instanceof \Traversable) {
1194
                foreach ($releases as $release) {
1195
                    $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1196
                    $passwordDeleted++;
1197
                }
1198
            }
1199
        }
1200
1201
        // Possibly passworded releases.
1202
        if ((int) Settings::settingValue('..deletepossiblerelease') === 1) {
1203
            $releases = $this->pdo->queryDirect(
1204
                sprintf(
1205
                    'SELECT SQL_NO_CACHE id, guid FROM releases WHERE passwordstatus = %d',
1206
                    Releases::PASSWD_POTENTIAL
1207
                )
1208
            );
1209
            if ($releases instanceof \Traversable) {
1210
                foreach ($releases as $release) {
1211
                    $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1212
                    $passwordDeleted++;
1213
                }
1214
            }
1215
        }
1216
1217
        if ((int) $this->crossPostTime !== 0) {
1218
            // Crossposted releases.
1219
            $releases = $this->pdo->queryDirect(
1220
                sprintf(
1221
                    'SELECT SQL_NO_CACHE id, guid FROM releases WHERE adddate > (NOW() - INTERVAL %d HOUR) GROUP BY name HAVING COUNT(name) > 1',
1222
                    $this->crossPostTime
1223
                )
1224
            );
1225
            if ($releases instanceof \Traversable) {
1226
                foreach ($releases as $release) {
1227
                    $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1228
                    $duplicateDeleted++;
1229
                }
1230
            }
1231
        }
1232
1233
        if ($this->completion > 0) {
1234
            $releases = $this->pdo->queryDirect(
1235
                sprintf('SELECT SQL_NO_CACHE id, guid FROM releases WHERE completion < %d AND completion > 0', $this->completion)
1236
            );
1237
            if ($releases instanceof \Traversable) {
1238
                foreach ($releases as $release) {
1239
                    $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1240
                    $completionDeleted++;
1241
                }
1242
            }
1243
        }
1244
1245
        // Disabled categories.
1246
        $disabledCategories = Category::getDisabledIDs();
1247
        if (\count($disabledCategories) > 0) {
1248
            foreach ($disabledCategories as $disabledCategory) {
1249
                $releases = $this->pdo->queryDirect(
1250
                    sprintf('SELECT SQL_NO_CACHE id, guid FROM releases WHERE categories_id = %d', (int) $disabledCategory['id'])
1251
                );
1252
                if ($releases instanceof \Traversable) {
1253
                    foreach ($releases as $release) {
1254
                        $disabledCategoryDeleted++;
1255
                        $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1256
                    }
1257
                }
1258
            }
1259
        }
1260
1261
        // Delete smaller than category minimum sizes.
1262
        $categories = $this->pdo->queryDirect(
1263
            '
1264
			SELECT SQL_NO_CACHE c.id AS id,
1265
			CASE WHEN c.minsizetoformrelease = 0 THEN cp.minsizetoformrelease ELSE c.minsizetoformrelease END AS minsize
1266
			FROM categories c
1267
			INNER JOIN categories cp ON cp.id = c.parentid
1268
			WHERE c.parentid IS NOT NULL'
1269
        );
1270
1271
        if ($categories instanceof \Traversable) {
1272
            foreach ($categories as $category) {
1273
                if ((int) $category['minsize'] > 0) {
1274
                    $releases = $this->pdo->queryDirect(
1275
                        sprintf(
1276
                            '
1277
							SELECT SQL_NO_CACHE r.id, r.guid
1278
							FROM releases r
1279
							WHERE r.categories_id = %d
1280
							AND r.size < %d
1281
							LIMIT 1000',
1282
                            (int) $category['id'],
1283
                            (int) $category['minsize']
1284
                        )
1285
                    );
1286
                    if ($releases instanceof \Traversable) {
1287
                        foreach ($releases as $release) {
1288
                            $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1289
                            $categoryMinSizeDeleted++;
1290
                        }
1291
                    }
1292
                }
1293
            }
1294
        }
1295
1296
        // Disabled music genres.
1297
        $genrelist = $genres->getDisabledIDs();
1298
        if (\count($genrelist) > 0) {
1299
            foreach ($genrelist as $genre) {
1300
                $releases = $this->pdo->queryDirect(
1301
                    sprintf(
1302
                        '
1303
						SELECT SQL_NO_CACHE id, guid
1304
						FROM releases
1305
						INNER JOIN
1306
						(
1307
							SELECT id AS mid
1308
							FROM musicinfo
1309
							WHERE musicinfo.genre_id = %d
1310
						) mi ON musicinfo_id = mid',
1311
                        (int) $genre['id']
1312
                    )
1313
                );
1314
                if ($releases instanceof \Traversable) {
1315
                    foreach ($releases as $release) {
1316
                        $disabledGenreDeleted++;
1317
                        $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1318
                    }
1319
                }
1320
            }
1321
        }
1322
1323
        // Misc other.
1324
        if (Settings::settingValue('..miscotherretentionhours') > 0) {
1325
            $releases = $this->pdo->queryDirect(
1326
                sprintf(
1327
                    '
1328
					SELECT SQL_NO_CACHE id, guid
1329
					FROM releases
1330
					WHERE categories_id = %d
1331
					AND adddate <= NOW() - INTERVAL %d HOUR',
1332
                    Category::OTHER_MISC,
1333
                    (int) Settings::settingValue('..miscotherretentionhours')
1334
                )
1335
            );
1336
            if ($releases instanceof \Traversable) {
1337
                foreach ($releases as $release) {
1338
                    $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1339
                    $miscRetentionDeleted++;
1340
                }
1341
            }
1342
        }
1343
1344
        // Misc hashed.
1345
        if ((int) Settings::settingValue('..mischashedretentionhours') > 0) {
1346
            $releases = $this->pdo->queryDirect(
1347
                sprintf(
1348
                    '
1349
					SELECT SQL_NO_CACHE id, guid
1350
					FROM releases
1351
					WHERE categories_id = %d
1352
					AND adddate <= NOW() - INTERVAL %d HOUR',
1353
                    Category::OTHER_HASHED,
1354
                    (int) Settings::settingValue('..mischashedretentionhours')
1355
                )
1356
            );
1357
            if ($releases instanceof \Traversable) {
1358
                foreach ($releases as $release) {
1359
                    $this->releases->deleteSingle(['g' => $release['guid'], 'i' => $release['id']], $this->nzb, $this->releaseImage);
1360
                    $miscHashedDeleted++;
1361
                }
1362
            }
1363
        }
1364
1365
        if ($this->echoCLI) {
1366
            ColorCLI::doEcho(
1367
                ColorCLI::primary(
1368
                    'Removed releases: '.
1369
                    number_format($retentionDeleted).
1370
                    ' past retention, '.
1371
                    number_format($passwordDeleted).
1372
                    ' passworded, '.
1373
                    number_format($duplicateDeleted).
1374
                    ' crossposted, '.
1375
                    number_format($disabledCategoryDeleted).
1376
                    ' from disabled categories, '.
1377
                    number_format($categoryMinSizeDeleted).
1378
                    ' smaller than category settings, '.
1379
                    number_format($disabledGenreDeleted).
1380
                    ' from disabled music genres, '.
1381
                    number_format($miscRetentionDeleted).
1382
                    ' from misc->other'.
1383
                    number_format($miscHashedDeleted).
1384
                    ' from misc->hashed'.
1385
                    (
1386
                        $this->completion > 0
1387
                        ? ', '.number_format($completionDeleted).' under '.$this->completion.'% completion.'
1388
                        : '.'
1389
                    )
1390
                ), true
1391
            );
1392
1393
            $totalDeleted = (
1394
                $retentionDeleted + $passwordDeleted + $duplicateDeleted + $disabledCategoryDeleted +
1395
                $disabledGenreDeleted + $miscRetentionDeleted + $miscHashedDeleted + $completionDeleted +
1396
                $categoryMinSizeDeleted
1397
            );
1398
            if ($totalDeleted > 0) {
1399
                ColorCLI::doEcho(
1400
                    ColorCLI::primary(
1401
                        'Removed '.number_format($totalDeleted).' releases in '.
1402
                        $this->consoleTools->convertTime(time() - $startTime)
1403
                    ), true
1404
                );
1405
            }
1406
        }
1407
    }
1408
1409
    /**
1410
     * Formulate part of a query to prevent deletion of currently inserting parts / binaries / collections.
1411
     *
1412
     * @param string $groupName
1413
     * @param int    $difference
1414
     *
1415
     * @return string
1416
     */
1417
    private function maxQueryFormulator($groupName, $difference): string
1418
    {
1419
        $maxID = $this->pdo->queryOneRow(
1420
            sprintf(
1421
                '
1422
				SELECT IFNULL(MAX(id),0) AS max
1423
				FROM %s',
1424
                $groupName
1425
            )
1426
        );
1427
1428
        return empty($maxID['max']) || $maxID['max'] < $difference ? 0 : $maxID['max'] - $difference;
1429
    }
1430
1431
    /**
1432
     * Look if we have all the files in a collection (which have the file count in the subject).
1433
     * Set file check to complete.
1434
     * This means the the binary table has the same count as the file count in the subject, but
1435
     * the collection might not be complete yet since we might not have all the articles in the parts table.
1436
     *
1437
     * @param string $where
1438
     *
1439
     * @void
1440
     */
1441
    private function collectionFileCheckStage1(&$where): void
1442
    {
1443
        $this->pdo->queryExec(
1444
            sprintf(
1445
                '
1446
				UPDATE %s c
1447
				INNER JOIN
1448
				(
1449
					SELECT c.id
1450
					FROM %s c
1451
					INNER JOIN %s b ON b.collections_id = c.id
1452
					WHERE c.totalfiles > 0
1453
					AND c.filecheck = %d %s
1454
					GROUP BY b.collections_id, c.totalfiles, c.id
1455
					HAVING COUNT(b.id) IN (c.totalfiles, c.totalfiles + 1)
1456
				) r ON c.id = r.id
1457
				SET filecheck = %d',
1458
                $this->tables['cname'],
1459
                $this->tables['cname'],
1460
                $this->tables['bname'],
1461
                self::COLLFC_DEFAULT,
1462
                $where,
1463
                self::COLLFC_COMPCOLL
1464
            )
1465
        );
1466
    }
1467
1468
    /**
1469
     * The first query sets filecheck to COLLFC_ZEROPART if there's a file that starts with 0 (ex. [00/100]).
1470
     * The second query sets filecheck to COLLFC_TEMPCOMP on everything left over, so anything that starts with 1 (ex. [01/100]).
1471
     *
1472
     * This is done because some collections start at 0 and some at 1, so if you were to assume the collection is complete
1473
     * at 0 then you would never get a complete collection if it starts with 1 and if it starts, you can end up creating
1474
     * a incomplete collection, since you assumed it was complete.
1475
     *
1476
     * @param string $where
1477
     *
1478
     * @void
1479
     */
1480
    private function collectionFileCheckStage2(&$where): void
1481
    {
1482
        $this->pdo->queryExec(
1483
            sprintf(
1484
                '
1485
				UPDATE %s c
1486
				INNER JOIN
1487
				(
1488
					SELECT c.id
1489
					FROM %s c
1490
					INNER JOIN %s b ON b.collections_id = c.id
1491
					WHERE b.filenumber = 0
1492
					AND c.totalfiles > 0
1493
					AND c.filecheck = %d %s
1494
					GROUP BY c.id
1495
				) r ON c.id = r.id
1496
				SET c.filecheck = %d',
1497
                $this->tables['cname'],
1498
                $this->tables['cname'],
1499
                $this->tables['bname'],
1500
                self::COLLFC_COMPCOLL,
1501
                $where,
1502
                self::COLLFC_ZEROPART
1503
            )
1504
        );
1505
        $this->pdo->queryExec(
1506
            sprintf(
1507
                '
1508
				UPDATE %s c
1509
				SET filecheck = %d
1510
				WHERE filecheck = %d %s',
1511
                $this->tables['cname'],
1512
                self::COLLFC_TEMPCOMP,
1513
                self::COLLFC_COMPCOLL,
1514
                $where
1515
            )
1516
        );
1517
    }
1518
1519
    /**
1520
     * Check if the files (binaries table) in a complete collection has all the parts.
1521
     * If we have all the parts, set binaries table partcheck to FILE_COMPLETE.
1522
     *
1523
     * @param string $where
1524
     *
1525
     * @void
1526
     */
1527
    private function collectionFileCheckStage3($where): void
1528
    {
1529
        $this->pdo->queryExec(
1530
            sprintf(
1531
                '
1532
				UPDATE %s b
1533
				INNER JOIN
1534
				(
1535
					SELECT b.id
1536
					FROM %s b
1537
					INNER JOIN %s c ON c.id = b.collections_id
1538
					WHERE c.filecheck = %d
1539
					AND b.partcheck = %d %s
1540
					AND b.currentparts = b.totalparts
1541
					GROUP BY b.id, b.totalparts
1542
				) r ON b.id = r.id
1543
				SET b.partcheck = %d',
1544
                $this->tables['bname'],
1545
                $this->tables['bname'],
1546
                $this->tables['cname'],
1547
                self::COLLFC_TEMPCOMP,
1548
                self::FILE_INCOMPLETE,
1549
                $where,
1550
                self::FILE_COMPLETE
1551
            )
1552
        );
1553
        $this->pdo->queryExec(
1554
            sprintf(
1555
                '
1556
				UPDATE %s b
1557
				INNER JOIN
1558
				(
1559
					SELECT b.id
1560
					FROM %s b
1561
					INNER JOIN %s c ON c.id = b.collections_id
1562
					WHERE c.filecheck = %d
1563
					AND b.partcheck = %d %s
1564
					AND b.currentparts >= (b.totalparts + 1)
1565
					GROUP BY b.id, b.totalparts
1566
				) r ON b.id = r.id
1567
				SET b.partcheck = %d',
1568
                $this->tables['bname'],
1569
                $this->tables['bname'],
1570
                $this->tables['cname'],
1571
                self::COLLFC_ZEROPART,
1572
                self::FILE_INCOMPLETE,
1573
                $where,
1574
                self::FILE_COMPLETE
1575
            )
1576
        );
1577
    }
1578
1579
    /**
1580
     * Check if all files (binaries table) for a collection are complete (if they all have the "parts").
1581
     * Set collections filecheck column to COLLFC_COMPPART.
1582
     * This means the collection is complete.
1583
     *
1584
     * @param string $where
1585
     *
1586
     * @void
1587
     */
1588
    private function collectionFileCheckStage4(&$where): void
1589
    {
1590
        $this->pdo->queryExec(
1591
            sprintf(
1592
                '
1593
				UPDATE %s c INNER JOIN
1594
					(SELECT c.id FROM %s c
1595
					INNER JOIN %s b ON c.id = b.collections_id
1596
					WHERE b.partcheck = 1 AND c.filecheck IN (%d, %d) %s
1597
					GROUP BY b.collections_id, c.totalfiles, c.id HAVING COUNT(b.id) >= c.totalfiles)
1598
				r ON c.id = r.id SET filecheck = %d',
1599
                $this->tables['cname'],
1600
                $this->tables['cname'],
1601
                $this->tables['bname'],
1602
                self::COLLFC_TEMPCOMP,
1603
                self::COLLFC_ZEROPART,
1604
                $where,
1605
                self::COLLFC_COMPPART
1606
            )
1607
        );
1608
    }
1609
1610
    /**
1611
     * If not all files (binaries table) had their parts on the previous stage,
1612
     * reset the collection filecheck column to COLLFC_COMPCOLL so we reprocess them next time.
1613
     *
1614
     * @param string $where
1615
     *
1616
     * @void
1617
     */
1618
    private function collectionFileCheckStage5(&$where): void
1619
    {
1620
        $this->pdo->queryExec(
1621
            sprintf(
1622
                '
1623
				UPDATE %s c
1624
				SET filecheck = %d
1625
				WHERE filecheck IN (%d, %d) %s',
1626
                $this->tables['cname'],
1627
                self::COLLFC_COMPCOLL,
1628
                self::COLLFC_TEMPCOMP,
1629
                self::COLLFC_ZEROPART,
1630
                $where
1631
            )
1632
        );
1633
    }
1634
1635
    /**
1636
     * If a collection did not have the file count (ie: [00/12]) or the collection is incomplete after
1637
     * $this->collectionDelayTime hours, set the collection to complete to create it into a release/nzb.
1638
     *
1639
     * @param string $where
1640
     *
1641
     * @void
1642
     */
1643
    private function collectionFileCheckStage6(&$where): void
1644
    {
1645
        $this->pdo->queryExec(
1646
            sprintf(
1647
                "
1648
				UPDATE %s c SET filecheck = %d, totalfiles = (SELECT COUNT(b.id) FROM %s b WHERE b.collections_id = c.id)
1649
				WHERE c.dateadded < NOW() - INTERVAL '%d' HOUR
1650
				AND c.filecheck IN (%d, %d, 10) %s",
1651
                $this->tables['cname'],
1652
                self::COLLFC_COMPPART,
1653
                $this->tables['bname'],
1654
                $this->collectionDelayTime,
1655
                self::COLLFC_DEFAULT,
1656
                self::COLLFC_COMPCOLL,
1657
                $where
1658
            )
1659
        );
1660
    }
1661
1662
    /**
1663
     * If a collection has been stuck for $this->collectionTimeout hours, delete it, it's bad.
1664
     *
1665
     * @param string $where
1666
     *
1667
     * @void
1668
     * @throws \Exception
1669
     */
1670
    private function processStuckCollections($where): void
1671
    {
1672
        $lastRun = Settings::settingValue('indexer.processing.last_run_time');
0 ignored issues
show
Bug introduced by
'indexer.processing.last_run_time' 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

1672
        $lastRun = Settings::settingValue(/** @scrutinizer ignore-type */ 'indexer.processing.last_run_time');
Loading history...
1673
1674
        $obj = $this->pdo->queryExec(
1675
            sprintf(
1676
                "
1677
                DELETE c FROM %s c
1678
                WHERE
1679
                    c.added <
1680
                    DATE_SUB({$this->pdo->escapeString($lastRun)}, INTERVAL %d HOUR)
1681
                %s",
1682
                $this->tables['cname'],
1683
                $this->collectionTimeout,
1684
                $where
1685
            )
1686
        );
1687
        if ($this->echoCLI && \is_object($obj) && $obj->rowCount()) {
1688
            ColorCLI::doEcho(
1689
                ColorCLI::primary('Deleted '.$obj->rowCount().' broken/stuck collections.'), true
1690
            );
1691
        }
1692
    }
1693
}
1694