Completed
Push — dev ( d8b313...239570 )
by Darko
07:26
created

ProcessReleases::collectionFileCheckStage3()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 49
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 42
dl 0
loc 49
ccs 0
cts 11
cp 0
rs 9.248
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
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\Genres;
10
use App\Models\Release;
11
use App\Models\Category;
12
use App\Models\Settings;
13
use Blacklight\ColorCLI;
14
use Blacklight\Releases;
15
use Blacklight\Categorize;
16
use Illuminate\Support\Str;
17
use App\Models\ReleaseRegex;
18
use Blacklight\ConsoleTools;
19
use Blacklight\ReleaseImage;
20
use App\Models\ReleasesGroups;
21
use Blacklight\ReleaseCleaning;
22
use Illuminate\Support\Facades\DB;
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 \PDO
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
     * Time (hours) to wait before delete a stuck/broken collection.
95
     *
96
     *
97
     * @var int
98
     */
99
    private $collectionTimeout;
100
101
    /**
102
     * @var \Blacklight\ColorCLI
103
     */
104
    protected $colorCli;
105
106
    /**
107
     * @param array $options Class instances / Echo to cli ?
108
     *
109
     * @throws \Exception
110
     */
111
    public function __construct(array $options = [])
112
    {
113
        $defaults = [
114
            'Echo'            => true,
115
            'ConsoleTools'    => null,
116
            'Groups'          => null,
117
            'NZB'             => null,
118
            'ReleaseCleaning' => null,
119
            'ReleaseImage'    => null,
120
            'Releases'        => null,
121
            'Settings'        => null,
122
        ];
123
        $options += $defaults;
124
125
        $this->echoCLI = ($options['Echo'] && config('nntmux.echocli'));
126
127
        $this->consoleTools = ($options['ConsoleTools'] instanceof ConsoleTools ? $options['ConsoleTools'] : new ConsoleTools());
128
        $this->nzb = ($options['NZB'] instanceof NZB ? $options['NZB'] : new NZB());
129
        $this->releaseCleaning = ($options['ReleaseCleaning'] instanceof ReleaseCleaning ? $options['ReleaseCleaning'] : new ReleaseCleaning());
130
        $this->releases = ($options['Releases'] instanceof Releases ? $options['Releases'] : new Releases(['Groups' => null]));
131
        $this->releaseImage = ($options['ReleaseImage'] instanceof ReleaseImage ? $options['ReleaseImage'] : new ReleaseImage());
132
        $this->colorCli = new ColorCLI();
133
134
        $dummy = Settings::settingValue('..delaytime');
135
        $this->collectionDelayTime = ($dummy !== '' ? (int) $dummy : 2);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
136
        $dummy = Settings::settingValue('..crossposttime');
137
        $this->crossPostTime = ($dummy !== '' ? (int) $dummy : 2);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
138
        $dummy = Settings::settingValue('..maxnzbsprocessed');
139
        $this->releaseCreationLimit = ($dummy !== '' ? (int) $dummy : 1000);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
140
        $dummy = Settings::settingValue('..completionpercent');
141
        $this->completion = ($dummy !== '' ? (int) $dummy : 0);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
142
        if ($this->completion > 100) {
143
            $this->completion = 100;
144
            $this->colorCli->error(PHP_EOL.'You have an invalid setting for completion. It cannot be higher than 100.');
145
        }
146
        $this->collectionTimeout = (int) Settings::settingValue('indexer.processing.collection_timeout');
147
    }
148
149
    /**
150
     * Main method for creating releases/NZB files from collections.
151
     *
152
     * @param int              $categorize
153
     * @param int              $postProcess
154
     * @param string           $groupName (optional)
155
     * @param \Blacklight\NNTP $nntp
156
     * @param bool             $echooutput
157
     *
158
     * @return int
159
     * @throws \Throwable
160
     */
161
    public function processReleases($categorize, $postProcess, $groupName, &$nntp, $echooutput): int
162
    {
163
        $this->echoCLI = ($echooutput && config('nntmux.echocli'));
164
        $groupID = '';
165
166
        if (! empty($groupName) && $groupName !== 'mgr') {
167
            $groupInfo = Group::getByName($groupName);
168
            if ($groupInfo !== null) {
169
                $groupID = $groupInfo['id'];
170
            }
171
        }
172
173
        if ($this->echoCLI) {
174
            $this->colorCli->header('Starting release update process ('.now()->format('Y-m-d H:i:s').')');
175
        }
176
177
        if (! file_exists(Settings::settingValue('..nzbpath'))) {
178
            if ($this->echoCLI) {
179
                $this->colorCli->error('Bad or missing nzb directory - '.Settings::settingValue('..nzbpath'));
180
            }
181
182
            return 0;
183
        }
184
185
        $this->processIncompleteCollections($groupID);
186
        $this->processCollectionSizes($groupID);
187
        $this->deleteUnwantedCollections($groupID);
188
189
        $totalReleasesAdded = 0;
190
        do {
191
            $releasesCount = $this->createReleases($groupID);
192
            $totalReleasesAdded += $releasesCount['added'];
193
194
            $nzbFilesAdded = $this->createNZBs($groupID);
195
196
            $this->categorizeReleases($categorize, $groupID);
197
            $this->postProcessReleases($postProcess, $nntp);
198
            $this->deleteCollections($groupID);
199
200
            // This loops as long as the number of releases or nzbs added was >= the limit (meaning there are more waiting to be created)
201
        } while (
202
            ($releasesCount['added'] + $releasesCount['dupes']) >= $this->releaseCreationLimit
203
            || $nzbFilesAdded >= $this->releaseCreationLimit
204
        );
205
206
        // Only run if non-mgr as mgr is not specific to group
207
        if ($groupName !== 'mgr') {
208
            $this->deletedReleasesByGroup($groupID);
209
            $this->deleteReleases();
210
        }
211
212
        return $totalReleasesAdded;
213
    }
214
215
    /**
216
     * Return all releases to other->misc category.
217
     *
218
     * @param string $where Optional "where" query parameter.
219
     *
220
     * @void
221
     */
222
    public function resetCategorize($where = ''): void
223
    {
224
        DB::update(
225
            sprintf('UPDATE releases SET categories_id = %d, iscategorized = 0 %s', Category::OTHER_MISC, $where)
226
        );
227
    }
228
229
    /**
230
     * Categorizes releases.
231
     *
232
     * @param string $type  name or searchname | Categorize using the search name or subject.
233
     * @param string $where Optional "where" query parameter.
234
     *
235
     * @return int Quantity of categorized releases.
236
     * @throws \Exception
237
     */
238
    public function categorizeRelease($type, $where = ''): int
239
    {
240
        $cat = new Categorize();
241
        $categorized = $total = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $total is dead and can be removed.
Loading history...
242
        $releases = Release::fromQuery(
243
            sprintf(
244
                '
245
				SELECT id, fromname, %s, groups_id
246
				FROM releases %s',
247
                $type,
248
                $where
249
            )
250
        );
251
        if (\count($releases) > 0) {
252
            $total = \count($releases);
253
            foreach ($releases as $release) {
254
                $catId = $cat->determineCategory($release->groups_id, $release->{$type}, $release->fromname);
255
                Release::query()->where('id', $release->id)->update(['categories_id' => $catId['categories_id'], 'iscategorized' => 1]);
256
                try {
257
                    $taggedRelease = Release::find($release->id);
258
                    $taggedRelease->retag($catId['tags']);
259
                } catch (\Throwable $e) {
260
                    //Just pass this part, tag is not created for some reason, exception is thrown and blocks release creation
261
                }
262
                $categorized++;
263
                if ($this->echoCLI) {
264
                    $this->consoleTools->overWritePrimary(
265
                        'Categorizing: '.$this->consoleTools->percentString($categorized, $total)
266
                    );
267
                }
268
            }
269
        }
270
        if ($this->echoCLI && $categorized > 0) {
271
            echo PHP_EOL;
272
        }
273
274
        return $categorized;
275
    }
276
277
    /**
278
     * @param $groupID
279
     *
280
     * @throws \Exception
281
     * @throws \Throwable
282
     */
283
    public function processIncompleteCollections($groupID): void
284
    {
285
        $startTime = now();
286
287
        if ($this->echoCLI) {
288
            $this->colorCli->header('Process Releases -> Attempting to find complete collections.');
289
        }
290
291
        $where = (! empty($groupID) ? ' AND c.groups_id = '.$groupID.' ' : ' ');
292
293
        $this->processStuckCollections($where);
294
        $this->collectionFileCheckStage1($where);
295
        $this->collectionFileCheckStage2($where);
296
        $this->collectionFileCheckStage3($where);
297
        $this->collectionFileCheckStage4($where);
298
        $this->collectionFileCheckStage5($where);
299
        $this->collectionFileCheckStage6($where);
300
301
        if ($this->echoCLI) {
302
            $count = DB::selectOne(
303
                sprintf(
304
                    '
305
					SELECT COUNT(c.id) AS complete
306
					FROM collections c
307
					WHERE c.filecheck = %d %s',
308
                    self::COLLFC_COMPPART,
309
                    $where
310
                )
311
            );
312
313
            $totalTime = now()->diffInSeconds($startTime);
314
315
            $this->colorCli->primary(
316
                    ($count === null ? 0 : $count->complete).' collections were found to be complete. Time: '.
317
                    $totalTime.Str::plural(' second', $totalTime),
318
                true
319
                );
320
        }
321
    }
322
323
    /**
324
     * @param $groupID
325
     *
326
     * @throws \Exception
327
     */
328
    public function processCollectionSizes($groupID): void
329
    {
330
        $startTime = now();
331
332
        if ($this->echoCLI) {
333
            $this->colorCli->header('Process Releases -> Calculating collection sizes (in bytes).');
334
        }
335
        // Get the total size in bytes of the collection for collections where filecheck = 2.
336
        DB::transaction(function () use ($groupID, $startTime) {
337
            $checked = DB::update(
338
                sprintf(
339
                    '
340
				UPDATE collections c
341
				SET c.filesize =
342
				(
343
					SELECT COALESCE(SUM(b.partsize), 0)
344
					FROM binaries b
345
					WHERE b.collections_id = c.id
346
				),
347
				c.filecheck = %d
348
				WHERE c.filecheck = %d
349
				AND c.filesize = 0 %s',
350
                    self::COLLFC_SIZED,
351
                    self::COLLFC_COMPPART,
352
                    (! empty($groupID) ? ' AND c.groups_id = '.$groupID : ' ')
353
                )
354
            );
355
            if ($checked > 0 && $this->echoCLI) {
356
                $this->colorCli->primary(
357
                    $checked.' collections set to filecheck = 3(size calculated)',
358
                    true
359
                );
360
                $totalTime = now()->diffInSeconds($startTime);
361
                $this->colorCli->primary($totalTime.Str::plural(' second', $totalTime), true);
362
            }
363
        }, 10);
364
    }
365
366
    /**
367
     * @param $groupID
368
     *
369
     * @throws \Exception
370
     * @throws \Throwable
371
     */
372
    public function deleteUnwantedCollections($groupID): void
373
    {
374
        $startTime = now();
375
376
        if ($this->echoCLI) {
377
            $this->colorCli->header('Process Releases -> Delete collections smaller/larger than minimum size/file count from group/site setting.');
378
        }
379
380
        $groupID === '' ? $groupIDs = Group::getActiveIDs() : $groupIDs = [['id' => $groupID]];
381
382
        $minSizeDeleted = $maxSizeDeleted = $minFilesDeleted = 0;
383
384
        $maxSizeSetting = Settings::settingValue('.release.maxsizetoformrelease');
385
        $minSizeSetting = Settings::settingValue('.release.minsizetoformrelease');
386
        $minFilesSetting = Settings::settingValue('.release.minfilestoformrelease');
387
388
        foreach ($groupIDs as $grpID) {
389
            $groupMinSizeSetting = $groupMinFilesSetting = 0;
390
391
            $groupMinimums = Group::getGroupByID($grpID['id']);
392
            if ($groupMinimums !== null) {
393
                if (! empty($groupMinimums['minsizetoformrelease']) && $groupMinimums['minsizetoformrelease'] > 0) {
394
                    $groupMinSizeSetting = (int) $groupMinimums['minsizetoformrelease'];
395
                }
396
                if (! empty($groupMinimums['minfilestoformrelease']) && $groupMinimums['minfilestoformrelease'] > 0) {
397
                    $groupMinFilesSetting = (int) $groupMinimums['minfilestoformrelease'];
398
                }
399
            }
400
401
            if (DB::selectOne(sprintf('
402
						SELECT SQL_NO_CACHE id
403
						FROM collections c
404
						WHERE c.filecheck = %d
405
						AND c.filesize > 0', self::COLLFC_SIZED)) !== null) {
406
                DB::transaction(function () use (
407
                    $groupMinSizeSetting,
408
                    $minSizeSetting,
409
                    $minSizeDeleted,
410
                    $maxSizeSetting,
411
                    $maxSizeDeleted,
412
                    $minFilesSetting,
413
                    $groupMinFilesSetting,
414
                    $minFilesDeleted,
415
                    $startTime
416
                ) {
417
                    $deleteQuery = DB::delete(sprintf('
418
						DELETE c FROM collections c
419
						WHERE c.filecheck = %d
420
						AND c.filesize > 0
421
						AND GREATEST(%d, %d) > 0
422
						AND c.filesize < GREATEST(%d, %d)', self::COLLFC_SIZED, $groupMinSizeSetting, $minSizeSetting, $groupMinSizeSetting, $minSizeSetting));
423
424
                    if ($deleteQuery > 0) {
425
                        $minSizeDeleted += $deleteQuery;
426
                    }
427
428
                    if ($maxSizeSetting > 0) {
429
                        $deleteQuery = DB::delete(sprintf('
430
							DELETE c FROM collections c
431
							WHERE c.filecheck = %d
432
							AND c.filesize > %d', self::COLLFC_SIZED, $maxSizeSetting));
433
434
                        if ($deleteQuery > 0) {
435
                            $maxSizeDeleted += $deleteQuery;
436
                        }
437
                    }
438
439
                    if ($minFilesSetting > 0 || $groupMinFilesSetting > 0) {
440
                        $deleteQuery = DB::delete(sprintf('
441
						DELETE c FROM collections c
442
						WHERE c.filecheck = %d
443
						AND GREATEST(%d, %d) > 0
444
						AND c.totalfiles < GREATEST(%d, %d)', self::COLLFC_SIZED, $groupMinFilesSetting, $minFilesSetting, $groupMinFilesSetting, $minFilesSetting));
445
446
                        if ($deleteQuery > 0) {
447
                            $minFilesDeleted += $deleteQuery;
448
                        }
449
                    }
450
451
                    $totalTime = now()->diffInSeconds($startTime);
452
453
                    if ($this->echoCLI) {
454
                        $this->colorCli->primary('Deleted '.($minSizeDeleted + $maxSizeDeleted + $minFilesDeleted).' collections: '.PHP_EOL.$minSizeDeleted.' smaller than, '.$maxSizeDeleted.' bigger than, '.$minFilesDeleted.' with less files than site/group settings in: '.$totalTime.Str::plural(' second', $totalTime), true);
455
                    }
456
                }, 10);
457
            }
458
        }
459
    }
460
461
    /**
462
     * @param int|string $groupID (optional)
463
     *
464
     * @return array
465
     * @throws \Throwable
466
     */
467
    public function createReleases($groupID): array
468
    {
469
        $startTime = now();
470
471
        $categorize = new Categorize();
472
        $returnCount = $duplicate = 0;
473
474
        if ($this->echoCLI) {
475
            $this->colorCli->header('Process Releases -> Create releases from complete collections.');
476
        }
477
478
        $collections = DB::select(
479
            sprintf(
480
                '
481
				SELECT SQL_NO_CACHE c.*, g.name AS gname
482
				FROM collections c
483
				INNER JOIN groups g ON c.groups_id = g.id
484
				WHERE %s c.filecheck = %d
485
				AND c.filesize > 0
486
				LIMIT %d',
487
                (! empty($groupID) ? ' c.groups_id = '.$groupID.' AND ' : ' '),
488
                self::COLLFC_SIZED,
489
                $this->releaseCreationLimit
490
            )
491
        );
492
493
        if ($this->echoCLI && \count($collections) > 0) {
494
            $this->colorCli->primary(\count($collections).' Collections ready to be converted to releases.', true);
495
        }
496
497
        foreach ($collections as $collection) {
498
            $cleanRelName = utf8_encode(str_replace(['#', '@', '$', '%', '^', '§', '¨', '©', 'Ö'], '', $collection->subject));
499
            $fromName = utf8_encode(
500
                    trim($collection->fromname, "'")
501
                );
502
503
            // Look for duplicates, duplicates match on releases.name, releases.fromname and releases.size
504
            // A 1% variance in size is considered the same size when the subject and poster are the same
505
            $dupeCheck = Release::query()
506
                    ->where(['name' => $cleanRelName, 'fromname' => $fromName])
507
                    ->whereBetween('size', [$collection->filesize * .99, $collection->filesize * 1.01])
508
                    ->first(['id']);
509
510
            if ($dupeCheck === null) {
511
                $cleanedName = $this->releaseCleaning->releaseCleaner(
512
                        $collection->subject,
513
                        $collection->fromname,
514
                        $collection->gname
515
                    );
516
517
                if (\is_array($cleanedName)) {
518
                    $properName = $cleanedName['properlynamed'];
519
                    $preID = $cleanedName['predb'] ?? false;
520
                    $cleanedName = $cleanedName['cleansubject'];
521
                } else {
522
                    $properName = true;
523
                    $preID = false;
524
                }
525
526
                if ($preID === false && $cleanedName !== '') {
527
                    // try to match the cleaned searchname to predb title or filename here
528
                    $preMatch = Predb::matchPre($cleanedName);
529
                    if ($preMatch !== false) {
530
                        $cleanedName = $preMatch['title'];
531
                        $preID = $preMatch['predb_id'];
532
                        $properName = true;
533
                    }
534
                }
535
536
                $determinedCategory = $categorize->determineCategory($collection->groups_id, $cleanedName);
537
538
                $releaseID = Release::insertRelease(
539
                        [
540
                            'name' => $cleanRelName,
541
                            'searchname' => ! empty($cleanedName) ? utf8_encode($cleanedName) : $cleanRelName,
542
                            'totalpart' => $collection->totalfiles,
543
                            'groups_id' => $collection->groups_id,
544
                            'guid' => createGUID(),
545
                            'postdate' => $collection->date,
546
                            'fromname' => $fromName,
547
                            'size' => $collection->filesize,
548
                            'categories_id' => $determinedCategory['categories_id'],
549
                            'isrenamed' => $properName === true ? 1 : 0,
550
                            'predb_id' => $preID === false ? 0 : $preID,
551
                            'nzbstatus' => NZB::NZB_NONE,
552
                        ]
553
                    );
554
                try {
555
                    $release = Release::find($releaseID);
556
                    $release->retag($determinedCategory['tags']);
557
                } catch (\Throwable $e) {
558
                    //Just pass this part, tag is not created for some reason, exception is thrown and blocks release creation
559
                }
560
561
                if ($releaseID !== null) {
562
                    // Update collections table to say we inserted the release.
563
                    DB::transaction(function () use ($collection, $releaseID) {
564
                        DB::update(
565
                            sprintf(
566
                                '
567
								UPDATE collections
568
								SET filecheck = %d, releases_id = %d
569
								WHERE id = %d',
570
                                self::COLLFC_INSERTED,
571
                                $releaseID,
572
                                $collection->id
573
                            )
574
                        );
575
                    }, 10);
576
577
                    // Add the id of regex that matched the collection and release name to release_regexes table
578
                    ReleaseRegex::insertIgnore([
579
                            'releases_id'            => $releaseID,
580
                            'collection_regex_id'    => $collection->collection_regexes_id,
581
                            'naming_regex_id'        => $cleanedName['id'] ?? 0,
582
                        ]);
583
584
                    if (preg_match_all('#(\S+):\S+#', $collection->xref, $matches)) {
585
                        foreach ($matches[1] as $grp) {
586
                            //check if the group name is in a valid format
587
                            $grpTmp = Group::isValidGroup($grp);
588
                            if ($grpTmp !== false) {
589
                                //check if the group already exists in database
590
                                $xrefGrpID = Group::getIDByName($grpTmp);
0 ignored issues
show
Bug introduced by
It seems like $grpTmp can also be of type true; however, parameter $name of App\Models\Group::getIDByName() 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

590
                                $xrefGrpID = Group::getIDByName(/** @scrutinizer ignore-type */ $grpTmp);
Loading history...
591
                                if ($xrefGrpID === '') {
592
                                    $xrefGrpID = Group::addGroup(
593
                                            [
594
                                                'name'                  => $grpTmp,
595
                                                'description'           => 'Added by Release processing',
596
                                                'backfill_target'       => 1,
597
                                                'first_record'          => 0,
598
                                                'last_record'           => 0,
599
                                                'active'                => 0,
600
                                                'backfill'              => 0,
601
                                                'minfilestoformrelease' => '',
602
                                                'minsizetoformrelease'  => '',
603
                                            ]
604
                                        );
605
                                }
606
607
                                $relGroupsChk = ReleasesGroups::query()->where(
608
                                        [
609
                                            ['releases_id', '=', $releaseID],
610
                                            ['groups_id', '=', $xrefGrpID],
611
                                        ]
612
                                    )->first();
613
614
                                if ($relGroupsChk === null) {
615
                                    ReleasesGroups::query()->insert(
616
                                            [
617
                                                'releases_id' => $releaseID,
618
                                                'groups_id'   => $xrefGrpID,
619
                                            ]
620
                                        );
621
                                }
622
                            }
623
                        }
624
                    }
625
626
                    $returnCount++;
627
628
                    if ($this->echoCLI) {
629
                        echo "Added $returnCount releases.\r";
630
                    }
631
                }
632
            } else {
633
                // The release was already in the DB, so delete the collection.
634
                DB::transaction(function () use ($collection) {
635
                    DB::delete(
636
                        sprintf(
637
                            '
638
							DELETE c
639
							FROM collections c
640
							WHERE c.collectionhash = %s',
641
                            escapeString($collection->collectionhash)
642
                        )
643
                    );
644
                }, 3);
645
646
                $duplicate++;
647
            }
648
        }
649
650
        $totalTime = now()->diffInSeconds($startTime);
651
652
        if ($this->echoCLI) {
653
            $this->colorCli->primary(
654
                    PHP_EOL.
655
                    number_format($returnCount).
656
                    ' Releases added and '.
657
                    number_format($duplicate).
658
                    ' duplicate collections deleted in '.
659
                    $totalTime.Str::plural(' second', $totalTime),
660
                true
661
                );
662
        }
663
664
        return ['added' => $returnCount, 'dupes' => $duplicate];
665
    }
666
667
    /**
668
     * Create NZB files from complete releases.
669
     *
670
     * @param int|string $groupID (optional)
671
     *
672
     * @return int
673
     * @throws \Throwable
674
     */
675
    public function createNZBs($groupID): int
676
    {
677
        $startTime = now();
678
679
        if ($this->echoCLI) {
680
            $this->colorCli->header('Process Releases -> Create the NZB, delete collections/binaries/parts.');
681
        }
682
683
        $releases = Release::fromQuery(
684
            sprintf(
685
                "
686
				SELECT SQL_NO_CACHE
687
					CONCAT(COALESCE(cp.title,'') , CASE WHEN cp.title IS NULL THEN '' ELSE ' > ' END , c.title) AS title,
688
					r.name, r.id, r.guid
689
				FROM releases r
690
				INNER JOIN categories c ON r.categories_id = c.id
691
				INNER JOIN categories cp ON cp.id = c.parentid
692
				WHERE %s nzbstatus = 0",
693
                ! empty($groupID) ? ' r.groups_id = '.$groupID.' AND ' : ' '
694
            )
695
        );
696
697
        $nzbCount = 0;
698
699
        if (\count($releases) > 0) {
700
            $total = \count($releases);
701
            // Init vars for writing the NZB's.
702
            $this->nzb->initiateForWrite();
703
            foreach ($releases as $release) {
704
                if ($this->nzb->writeNzbForReleaseId($release->id, $release->guid, $release->name, $release->title)) {
0 ignored issues
show
Bug introduced by
The property title does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
705
                    $nzbCount++;
706
                    if ($this->echoCLI) {
707
                        echo "Creating NZBs and deleting Collections: $nzbCount/$total.\r";
708
                    }
709
                }
710
            }
711
        }
712
713
        $totalTime = now()->diffInSeconds($startTime);
714
715
        if ($this->echoCLI) {
716
            $this->colorCli->primary(
717
                    number_format($nzbCount).' NZBs created/Collections deleted in '.
718
                    $totalTime.Str::plural(' second', $totalTime).PHP_EOL.
719
                    'Total time: '.$totalTime.Str::plural(' second', $totalTime),
720
                true
721
            );
722
        }
723
724
        return $nzbCount;
725
    }
726
727
    /**
728
     * Categorize releases.
729
     *
730
     * @param int        $categorize
731
     * @param int|string $groupID (optional)
732
     *
733
     * @void
734
     * @throws \Exception
735
     */
736
    public function categorizeReleases($categorize, $groupID = ''): void
737
    {
738
        $startTime = now();
739
        if ($this->echoCLI) {
740
            $this->colorCli->header('Process Releases -> Categorize releases.');
741
        }
742
        switch ((int) $categorize) {
743
            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...
744
                $type = 'searchname';
745
                break;
746
            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...
747
            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...
748
749
                $type = 'name';
750
                break;
751
        }
752
        $this->categorizeRelease(
753
            $type,
754
            (! empty($groupID)
755
                ? 'WHERE categories_id = '.Category::OTHER_MISC.' AND iscategorized = 0 AND groups_id = '.$groupID
756
                : 'WHERE categories_id = '.Category::OTHER_MISC.' AND iscategorized = 0')
757
        );
758
759
        $totalTime = now()->diffInSeconds($startTime);
760
761
        if ($this->echoCLI) {
762
            $this->colorCli->primary($totalTime.Str::plural(' second', $totalTime));
763
        }
764
    }
765
766
    /**
767
     * Post-process releases.
768
     *
769
     * @param int  $postProcess
770
     * @param NNTP $nntp
771
     *
772
     * @void
773
     * @throws \Exception
774
     */
775
    public function postProcessReleases($postProcess, &$nntp): void
776
    {
777
        if ((int) $postProcess === 1) {
778
            (new PostProcess(['Echo' => $this->echoCLI]))->processAll($nntp);
779
        } elseif ($this->echoCLI) {
780
            $this->colorCli->info(
781
                    'Post-processing is not running inside the Process Releases class.'.PHP_EOL.
782
                    'If you are using tmux or screen they might have their own scripts running Post-processing.'
783
                );
784
        }
785
    }
786
787
    /**
788
     * @param $groupID
789
     *
790
     * @throws \Exception
791
     * @throws \Throwable
792
     */
793
    public function deleteCollections($groupID): void
0 ignored issues
show
Unused Code introduced by
The parameter $groupID is not used and could be removed. ( Ignorable by Annotation )

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

793
    public function deleteCollections(/** @scrutinizer ignore-unused */ $groupID): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
794
    {
795
        $startTime = now();
796
797
        $deletedCount = 0;
798
799
        // CBP older than retention.
800
        if ($this->echoCLI) {
801
            echo
802
                $this->colorCli->header('Process Releases -> Delete finished collections.'.PHP_EOL).
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->header(...ght\processing\PHP_EOL) targeting Blacklight\ColorCLI::header() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
Are you sure $this->colorCli->header(...ght\processing\PHP_EOL) of type void can be used in concatenation? ( Ignorable by Annotation )

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

802
                /** @scrutinizer ignore-type */ $this->colorCli->header('Process Releases -> Delete finished collections.'.PHP_EOL).
Loading history...
803
                $this->colorCli->primary(sprintf(
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->primary...etentionhours')), true) targeting Blacklight\ColorCLI::primary() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
804
                    'Deleting collections/binaries/parts older than %d hours.',
805
                    Settings::settingValue('..partretentionhours')
806
                ), true);
807
        }
808
809
        DB::transaction(function () use ($deletedCount, $startTime) {
810
            $deleted = 0;
811
            $deleteQuery = DB::delete(
812
                sprintf(
813
                    '
814
				DELETE c
815
				FROM collections c
816
				WHERE (c.dateadded < NOW() - INTERVAL %d HOUR)',
817
                    Settings::settingValue('..partretentionhours')
818
                )
819
            );
820
            if ($deleteQuery > 0) {
821
                $deleted = $deleteQuery;
822
                $deletedCount += $deleted;
823
            }
824
            $firstQuery = $fourthQuery = now();
825
826
            $totalTime = $firstQuery->diffInSeconds($startTime);
827
828
            if ($this->echoCLI) {
829
                $this->colorCli->primary(
830
                    'Finished deleting '.$deleted.' old collections/binaries/parts in '.
831
                    $totalTime.Str::plural(' second', $totalTime),
832
                    true
833
                );
834
            }
835
836
            // Cleanup orphaned collections, binaries and parts
837
            // this really shouldn't happen, but just incase - so we only run 1/200 of the time
838
            if (random_int(0, 200) <= 1) {
839
                // CBP collection orphaned with no binaries or parts.
840
                if ($this->echoCLI) {
841
                    echo
842
                        $this->colorCli->header('Process Releases -> Remove CBP orphans.'.PHP_EOL).
0 ignored issues
show
Bug introduced by
Are you sure $this->colorCli->header(...ght\processing\PHP_EOL) of type void can be used in concatenation? ( Ignorable by Annotation )

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

842
                        /** @scrutinizer ignore-type */ $this->colorCli->header('Process Releases -> Remove CBP orphans.'.PHP_EOL).
Loading history...
Bug introduced by
Are you sure the usage of $this->colorCli->header(...ght\processing\PHP_EOL) targeting Blacklight\ColorCLI::header() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
843
                        $this->colorCli->primary('Deleting orphaned collections.');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->primary...orphaned collections.') targeting Blacklight\ColorCLI::primary() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
844
                }
845
846
                $deleted = 0;
847
                $deleteQuery =
848
                    DB::delete(
849
                        '
850
					DELETE c, b, p
851
					FROM collections c
852
					LEFT JOIN binaries b ON c.id = b.collections_id
853
					LEFT JOIN parts p ON b.id = p.binaries_id
854
					WHERE (b.id IS NULL OR p.binaries_id IS NULL)'
855
                    );
856
857
                if ($deleteQuery > 0) {
858
                    $deleted = $deleteQuery;
859
                    $deletedCount += $deleted;
860
                }
861
862
                $secondQuery = now();
863
                $totalTime = $secondQuery->diffInSeconds($firstQuery);
864
865
                if ($this->echoCLI) {
866
                    $this->colorCli->primary(
867
                        'Finished deleting '.$deleted.' orphaned collections in '.
868
                        $totalTime.Str::plural(' second', $totalTime),
869
                        true
870
                    );
871
                }
872
873
                // orphaned binaries - binaries with no parts or binaries with no collection
874
                // Don't delete currently inserting binaries by checking the max id.
875
                if ($this->echoCLI) {
876
                    $this->colorCli->primary('Deleting orphaned binaries/parts with no collection.', true);
877
                }
878
879
                $deleted = 0;
880
                $deleteQuery =
881
                    DB::delete(
882
                        sprintf(
883
                            'DELETE b, p FROM binaries b
884
					LEFT JOIN parts p ON b.id = p.binaries_id
885
					LEFT JOIN collections c ON b.collections_id = c.id
886
					WHERE (p.binaries_id IS NULL OR c.id IS NULL)
887
					AND b.id < %d',
888
                            $this->maxQueryFormulator('binaries', 20000)
889
                        )
890
                    );
891
892
                if ($deleteQuery > 0) {
893
                    $deleted = $deleteQuery;
894
                    $deletedCount += $deleted;
895
                }
896
897
                $thirdQuery = now();
898
899
                $totalTime = $thirdQuery->diffInSeconds($secondQuery);
900
901
                if ($this->echoCLI) {
902
                    $this->colorCli->primary(
903
                        'Finished deleting '.$deleted.' binaries with no collections or parts in '.$totalTime.Str::plural(' second', $totalTime),
904
                        true
905
                    );
906
                }
907
908
                // orphaned parts - parts with no binary
909
                // Don't delete currently inserting parts by checking the max id.
910
                if ($this->echoCLI) {
911
                    $this->colorCli->primary('Deleting orphaned parts with no binaries.', true);
912
                }
913
                $deleted = 0;
914
                $deleteQuery =
915
                    DB::delete(
916
                        sprintf(
917
                            '
918
					DELETE p
919
					FROM parts p
920
					LEFT JOIN binaries b ON p.binaries_id = b.id
921
					WHERE b.id IS NULL
922
					AND p.binaries_id < %d',
923
                            $this->maxQueryFormulator('binaries', 20000)
924
                        )
925
                    );
926
927
                if ($deleteQuery > 0) {
928
                    $deleted = $deleteQuery;
929
                    $deletedCount += $deleted;
930
                }
931
932
                $fourthQuery = now();
933
934
                $totalTime = $fourthQuery->diffInSeconds($thirdQuery);
935
936
                if ($this->echoCLI) {
937
                    $this->colorCli->primary(
938
                        'Finished deleting '.$deleted.' parts with no binaries in '.
939
                        $totalTime.Str::plural(' second', $totalTime),
940
                        true
941
                    );
942
                }
943
            } // done cleaning up Binaries/Parts orphans
944
945
            if ($this->echoCLI) {
946
                $this->colorCli->primary(
947
                    'Deleting collections that were missed after NZB creation.',
948
                    true
949
                );
950
            }
951
952
            $deleted = 0;
953
            // Collections that were missing on NZB creation.
954
            $collections = DB::select(
955
                '
956
				SELECT SQL_NO_CACHE c.id
957
				FROM collections c
958
				INNER JOIN releases r ON r.id = c.releases_id
959
				WHERE r.nzbstatus = 1'
960
            );
961
962
            foreach ($collections as $collection) {
963
                $deleted++;
964
                DB::delete(
965
                    sprintf(
966
                        '
967
						DELETE c
968
						FROM collections c
969
						WHERE c.id = %d',
970
                        $collection->id
971
                    )
972
                );
973
            }
974
            $deletedCount += $deleted;
975
976
            $colDelTime = now()->diffInSeconds($fourthQuery);
977
            $totalTime = $fourthQuery->diffInSeconds($startTime);
978
979
            if ($this->echoCLI) {
980
                $this->colorCli->primary(
981
                    'Finished deleting '.$deleted.' collections missed after NZB creation in '.
982
                    $colDelTime.Str::plural(' second', $colDelTime).PHP_EOL.
983
                    'Removed '.
984
                    number_format($deletedCount).
985
                    ' parts/binaries/collection rows in '.
986
                    $totalTime.Str::plural(' second', $totalTime),
987
                    true
988
                );
989
            }
990
        }, 10);
991
    }
992
993
    /**
994
     * Delete unwanted releases based on admin settings.
995
     * This deletes releases based on group.
996
     *
997
     * @param int|string $groupID (optional)
998
     *
999
     * @void
1000
     * @throws \Exception
1001
     */
1002
    public function deletedReleasesByGroup($groupID = ''): void
1003
    {
1004
        $startTime = now();
1005
        $minSizeDeleted = $maxSizeDeleted = $minFilesDeleted = 0;
1006
1007
        if ($this->echoCLI) {
1008
            $this->colorCli->header('Process Releases -> Delete releases smaller/larger than minimum size/file count from group/site setting.');
1009
        }
1010
1011
        $groupID === '' ? $groupIDs = Group::getActiveIDs() : $groupIDs = [['id' => $groupID]];
1012
1013
        $maxSizeSetting = Settings::settingValue('.release.maxsizetoformrelease');
1014
        $minSizeSetting = Settings::settingValue('.release.minsizetoformrelease');
1015
        $minFilesSetting = Settings::settingValue('.release.minfilestoformrelease');
1016
1017
        foreach ($groupIDs as $grpID) {
1018
            $releases = Release::fromQuery(
1019
                sprintf(
1020
                    '
1021
					SELECT SQL_NO_CACHE r.guid, r.id
1022
					FROM releases r
1023
					INNER JOIN groups g ON g.id = r.groups_id
1024
					WHERE r.groups_id = %d
1025
					AND greatest(IFNULL(g.minsizetoformrelease, 0), %d) > 0
1026
					AND r.size < greatest(IFNULL(g.minsizetoformrelease, 0), %d)',
1027
                    $grpID['id'],
1028
                    $minSizeSetting,
1029
                    $minSizeSetting
1030
                )
1031
            );
1032
            foreach ($releases as $release) {
1033
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1034
                $minSizeDeleted++;
1035
            }
1036
1037
            if ($maxSizeSetting > 0) {
1038
                $releases = Release::fromQuery(
1039
                    sprintf(
1040
                        '
1041
						SELECT SQL_NO_CACHE id, guid
1042
						FROM releases
1043
						WHERE groups_id = %d
1044
						AND size > %d',
1045
                        $grpID['id'],
1046
                        $maxSizeSetting
1047
                    )
1048
                );
1049
                foreach ($releases as $release) {
1050
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1051
                    $maxSizeDeleted++;
1052
                }
1053
            }
1054
            if ($minFilesSetting > 0) {
1055
                $releases = Release::fromQuery(
1056
                     sprintf(
1057
                         '
1058
				SELECT SQL_NO_CACHE r.id, r.guid
1059
				FROM releases r
1060
				INNER JOIN groups g ON g.id = r.groups_id
1061
				WHERE r.groups_id = %d
1062
				AND greatest(IFNULL(g.minfilestoformrelease, 0), %d) > 0
1063
				AND r.totalpart < greatest(IFNULL(g.minfilestoformrelease, 0), %d)',
1064
                         $grpID['id'],
1065
                         $minFilesSetting,
1066
                         $minFilesSetting
1067
                     )
1068
                 );
1069
                foreach ($releases as $release) {
1070
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1071
                    $minFilesDeleted++;
1072
                }
1073
            }
1074
        }
1075
1076
        $totalTime = now()->diffInSeconds($startTime);
1077
1078
        if ($this->echoCLI) {
1079
            $this->colorCli->primary(
1080
                    'Deleted '.($minSizeDeleted + $maxSizeDeleted + $minFilesDeleted).
1081
                    ' releases: '.PHP_EOL.
1082
                    $minSizeDeleted.' smaller than, '.$maxSizeDeleted.' bigger than, '.$minFilesDeleted.
1083
                    ' with less files than site/groups setting in: '.
1084
                    $totalTime.Str::plural(' second', $totalTime),
1085
                true
1086
                );
1087
        }
1088
    }
1089
1090
    /**
1091
     * Delete releases using admin settings.
1092
     * This deletes releases, regardless of group.
1093
     *
1094
     * @void
1095
     * @throws \Exception
1096
     */
1097
    public function deleteReleases(): void
1098
    {
1099
        $startTime = now();
1100
        $genres = new Genres();
1101
        $passwordDeleted = $duplicateDeleted = $retentionDeleted = $completionDeleted = $disabledCategoryDeleted = 0;
1102
        $disabledGenreDeleted = $miscRetentionDeleted = $miscHashedDeleted = $categoryMinSizeDeleted = 0;
1103
1104
        // Delete old releases and finished collections.
1105
        if ($this->echoCLI) {
1106
            $this->colorCli->header('Process Releases -> Delete old releases and passworded releases.');
1107
        }
1108
1109
        // Releases past retention.
1110
        if ((int) Settings::settingValue('..releaseretentiondays') !== 0) {
1111
            $releases = Release::fromQuery(
1112
                sprintf(
1113
                    'SELECT id, guid FROM releases WHERE postdate < (NOW() - INTERVAL %d DAY)',
1114
                    (int) Settings::settingValue('..releaseretentiondays')
1115
                )
1116
            );
1117
            foreach ($releases as $release) {
1118
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1119
                $retentionDeleted++;
1120
            }
1121
        }
1122
1123
        // Passworded releases.
1124
        if ((int) Settings::settingValue('..deletepasswordedrelease') === 1) {
1125
            $releases = Release::fromQuery(
1126
                sprintf(
1127
                    'SELECT id, guid FROM releases WHERE passwordstatus = %d',
1128
                    Releases::PASSWD_RAR
1129
                )
1130
            );
1131
            foreach ($releases as $release) {
1132
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1133
                $passwordDeleted++;
1134
            }
1135
        }
1136
1137
        // Possibly passworded releases.
1138
        if ((int) Settings::settingValue('..deletepossiblerelease') === 1) {
1139
            $releases = Release::fromQuery(
1140
                sprintf(
1141
                    'SELECT id, guid FROM releases WHERE passwordstatus = %d',
1142
                    Releases::PASSWD_POTENTIAL
1143
                )
1144
            );
1145
            foreach ($releases as $release) {
1146
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1147
                $passwordDeleted++;
1148
            }
1149
        }
1150
1151
        if ((int) $this->crossPostTime !== 0) {
1152
            // Crossposted releases.
1153
            $releases = Release::fromQuery(
1154
                sprintf(
1155
                    'SELECT id, guid FROM releases WHERE adddate > (NOW() - INTERVAL %d HOUR) GROUP BY name HAVING COUNT(name) > 1',
1156
                    $this->crossPostTime
1157
                )
1158
            );
1159
            foreach ($releases as $release) {
1160
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1161
                $duplicateDeleted++;
1162
            }
1163
        }
1164
1165
        if ($this->completion > 0) {
1166
            $releases = Release::fromQuery(
1167
                sprintf('SELECT id, guid FROM releases WHERE completion < %d AND completion > 0', $this->completion)
1168
            );
1169
            foreach ($releases as $release) {
1170
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1171
                $completionDeleted++;
1172
            }
1173
        }
1174
1175
        // Disabled categories.
1176
        $disabledCategories = Category::getDisabledIDs();
1177
        if (\count($disabledCategories) > 0) {
1178
            foreach ($disabledCategories as $disabledCategory) {
1179
                $releases = Release::fromQuery(
1180
                    sprintf('SELECT id, guid FROM releases WHERE categories_id = %d', (int) $disabledCategory['id'])
1181
                );
1182
                foreach ($releases as $release) {
1183
                    $disabledCategoryDeleted++;
1184
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1185
                }
1186
            }
1187
        }
1188
1189
        // Delete smaller than category minimum sizes.
1190
        $categories = Category::fromQuery(
1191
            '
1192
			SELECT SQL_NO_CACHE c.id AS id,
1193
			CASE WHEN c.minsizetoformrelease = 0 THEN cp.minsizetoformrelease ELSE c.minsizetoformrelease END AS minsize
1194
			FROM categories c
1195
			INNER JOIN categories cp ON cp.id = c.parentid
1196
			WHERE c.parentid IS NOT NULL'
1197
        );
1198
1199
        foreach ($categories as $category) {
1200
            if ((int) $category->minsize > 0) {
0 ignored issues
show
Bug introduced by
The property minsize does not exist on App\Models\Category. Did you mean minsizetoformrelease?
Loading history...
1201
                $releases = Release::fromQuery(
1202
                        sprintf(
1203
                            '
1204
							SELECT id, guid
1205
							FROM releases
1206
							WHERE categories_id = %d
1207
							AND size < %d
1208
							LIMIT 1000',
1209
                            (int) $category->id,
1210
                            (int) $category->minsize
1211
                        )
1212
                    );
1213
                foreach ($releases as $release) {
1214
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1215
                    $categoryMinSizeDeleted++;
1216
                }
1217
            }
1218
        }
1219
1220
        // Disabled music genres.
1221
        $genrelist = $genres->getDisabledIDs();
1222
        if (\count($genrelist) > 0) {
1223
            foreach ($genrelist as $genre) {
1224
                $releases = Release::fromQuery(
1225
                    sprintf(
1226
                        '
1227
						SELECT id, guid
1228
						FROM releases r
1229
						INNER JOIN
1230
						(
1231
							SELECT id AS mid
1232
							FROM musicinfo
1233
							WHERE musicinfo.genre_id = %d
1234
						) mi ON musicinfo_id = mi.mid',
1235
                        (int) $genre['id']
1236
                    )
1237
                );
1238
                foreach ($releases as $release) {
1239
                    $disabledGenreDeleted++;
1240
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1241
                }
1242
            }
1243
        }
1244
1245
        // Misc other.
1246
        if (Settings::settingValue('..miscotherretentionhours') > 0) {
1247
            $releases = Release::fromQuery(
1248
                sprintf(
1249
                    '
1250
					SELECT SQL_NO_CACHE id, guid
1251
					FROM releases
1252
					WHERE categories_id = %d
1253
					AND adddate <= NOW() - INTERVAL %d HOUR',
1254
                    Category::OTHER_MISC,
1255
                    (int) Settings::settingValue('..miscotherretentionhours')
1256
                )
1257
            );
1258
            foreach ($releases as $release) {
1259
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1260
                $miscRetentionDeleted++;
1261
            }
1262
        }
1263
1264
        // Misc hashed.
1265
        if ((int) Settings::settingValue('..mischashedretentionhours') > 0) {
1266
            $releases = Release::fromQuery(
1267
                sprintf(
1268
                    '
1269
					SELECT SQL_NO_CACHE id, guid
1270
					FROM releases
1271
					WHERE categories_id = %d
1272
					AND adddate <= NOW() - INTERVAL %d HOUR',
1273
                    Category::OTHER_HASHED,
1274
                    (int) Settings::settingValue('..mischashedretentionhours')
1275
                )
1276
            );
1277
            foreach ($releases as $release) {
1278
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1279
                $miscHashedDeleted++;
1280
            }
1281
        }
1282
1283
        if ($this->echoCLI) {
1284
            $this->colorCli->primary(
1285
                    'Removed releases: '.
1286
                    number_format($retentionDeleted).
1287
                    ' past retention, '.
1288
                    number_format($passwordDeleted).
1289
                    ' passworded, '.
1290
                    number_format($duplicateDeleted).
1291
                    ' crossposted, '.
1292
                    number_format($disabledCategoryDeleted).
1293
                    ' from disabled categories, '.
1294
                    number_format($categoryMinSizeDeleted).
1295
                    ' smaller than category settings, '.
1296
                    number_format($disabledGenreDeleted).
1297
                    ' from disabled music genres, '.
1298
                    number_format($miscRetentionDeleted).
1299
                    ' from misc->other '.
1300
                    number_format($miscHashedDeleted).
1301
                    ' from misc->hashed'.
1302
                    (
1303
                        $this->completion > 0
1304
                        ? ', '.number_format($completionDeleted).' under '.$this->completion.'% completion.'
1305
                        : '.'
1306
                    ),
1307
                true
1308
                );
1309
1310
            $totalDeleted = (
1311
                $retentionDeleted + $passwordDeleted + $duplicateDeleted + $disabledCategoryDeleted +
1312
                $disabledGenreDeleted + $miscRetentionDeleted + $miscHashedDeleted + $completionDeleted +
1313
                $categoryMinSizeDeleted
1314
            );
1315
            if ($totalDeleted > 0) {
1316
                $totalTime = now()->diffInSeconds($startTime);
1317
                $this->colorCli->primary(
1318
                        'Removed '.number_format($totalDeleted).' releases in '.
1319
                        $totalTime.Str::plural(' second', $totalTime),
1320
                    true
1321
                    );
1322
            }
1323
        }
1324
    }
1325
1326
    /**
1327
     * Formulate part of a query to prevent deletion of currently inserting parts / binaries / collections.
1328
     *
1329
     * @param string $groupName
1330
     * @param int    $difference
1331
     *
1332
     * @return string
1333
     */
1334
    private function maxQueryFormulator($groupName, $difference): string
1335
    {
1336
        $maxID = DB::selectOne(
1337
            sprintf(
1338
                '
1339
				SELECT IFNULL(MAX(id),0) AS max
1340
				FROM %s',
1341
                $groupName
1342
            )
1343
        );
1344
1345
        return empty($maxID->max) || $maxID->max < $difference ? 0 : $maxID->max - $difference;
1346
    }
1347
1348
    /**
1349
     * Look if we have all the files in a collection (which have the file count in the subject).
1350
     * Set file check to complete.
1351
     * This means the the binary table has the same count as the file count in the subject, but
1352
     * the collection might not be complete yet since we might not have all the articles in the parts table.
1353
     *
1354
     * @param string $where
1355
     *
1356
     * @void
1357
     * @throws \Throwable
1358
     */
1359
    private function collectionFileCheckStage1(&$where): void
1360
    {
1361
        DB::transaction(function () use ($where) {
1362
            DB::update(
1363
                sprintf(
1364
                    '
1365
				UPDATE collections c
1366
				INNER JOIN
1367
				(
1368
					SELECT c.id
1369
					FROM collections c
1370
					INNER JOIN binaries b ON b.collections_id = c.id
1371
					WHERE c.totalfiles > 0
1372
					AND c.filecheck = %d %s
1373
					GROUP BY b.collections_id, c.totalfiles, c.id
1374
					HAVING COUNT(b.id) IN (c.totalfiles, c.totalfiles + 1)
1375
				) r ON c.id = r.id
1376
				SET filecheck = %d',
1377
                    self::COLLFC_DEFAULT,
1378
                    $where,
1379
                    self::COLLFC_COMPCOLL
1380
                )
1381
            );
1382
        }, 10);
1383
    }
1384
1385
    /**
1386
     * The first query sets filecheck to COLLFC_ZEROPART if there's a file that starts with 0 (ex. [00/100]).
1387
     * The second query sets filecheck to COLLFC_TEMPCOMP on everything left over, so anything that starts with 1 (ex. [01/100]).
1388
     *
1389
     * This is done because some collections start at 0 and some at 1, so if you were to assume the collection is complete
1390
     * at 0 then you would never get a complete collection if it starts with 1 and if it starts, you can end up creating
1391
     * a incomplete collection, since you assumed it was complete.
1392
     *
1393
     * @param string $where
1394
     *
1395
     * @void
1396
     * @throws \Throwable
1397
     */
1398
    private function collectionFileCheckStage2(&$where): void
1399
    {
1400
        DB::transaction(function () use ($where) {
1401
            DB::update(
1402
                sprintf(
1403
                    '
1404
				UPDATE collections c
1405
				INNER JOIN
1406
				(
1407
					SELECT c.id
1408
					FROM collections c
1409
					INNER JOIN binaries b ON b.collections_id = c.id
1410
					WHERE b.filenumber = 0
1411
					AND c.totalfiles > 0
1412
					AND c.filecheck = %d %s
1413
					GROUP BY c.id
1414
				) r ON c.id = r.id
1415
				SET c.filecheck = %d',
1416
                    self::COLLFC_COMPCOLL,
1417
                    $where,
1418
                    self::COLLFC_ZEROPART
1419
                )
1420
            );
1421
        }, 10);
1422
1423
        DB::transaction(function () use ($where) {
1424
            DB::update(
1425
                sprintf(
1426
                    '
1427
				UPDATE collections c
1428
				SET filecheck = %d
1429
				WHERE filecheck = %d %s',
1430
                    self::COLLFC_TEMPCOMP,
1431
                    self::COLLFC_COMPCOLL,
1432
                    $where
1433
                )
1434
            );
1435
        }, 10);
1436
    }
1437
1438
    /**
1439
     * Check if the files (binaries table) in a complete collection has all the parts.
1440
     * If we have all the parts, set binaries table partcheck to FILE_COMPLETE.
1441
     *
1442
     * @param string $where
1443
     *
1444
     * @void
1445
     * @throws \Throwable
1446
     */
1447
    private function collectionFileCheckStage3($where): void
1448
    {
1449
        DB::transaction(function () use ($where) {
1450
            DB::update(
1451
            sprintf(
1452
                '
1453
				UPDATE binaries b
1454
				INNER JOIN
1455
				(
1456
					SELECT b.id
1457
					FROM binaries b
1458
					INNER JOIN collections c ON c.id = b.collections_id
1459
					WHERE c.filecheck = %d
1460
					AND b.partcheck = %d %s
1461
					AND b.currentparts = b.totalparts
1462
					GROUP BY b.id, b.totalparts
1463
				) r ON b.id = r.id
1464
				SET b.partcheck = %d',
1465
                self::COLLFC_TEMPCOMP,
1466
                self::FILE_INCOMPLETE,
1467
                $where,
1468
                self::FILE_COMPLETE
1469
            )
1470
        );
1471
        }, 10);
1472
1473
        DB::transaction(function () use ($where) {
1474
            DB::update(
1475
                sprintf(
1476
                    '
1477
				UPDATE binaries b
1478
				INNER JOIN
1479
				(
1480
					SELECT b.id
1481
					FROM binaries b
1482
					INNER JOIN collections c ON c.id = b.collections_id
1483
					WHERE c.filecheck = %d
1484
					AND b.partcheck = %d %s
1485
					AND b.currentparts >= (b.totalparts + 1)
1486
					GROUP BY b.id, b.totalparts
1487
				) r ON b.id = r.id
1488
				SET b.partcheck = %d',
1489
                    self::COLLFC_ZEROPART,
1490
                    self::FILE_INCOMPLETE,
1491
                    $where,
1492
                    self::FILE_COMPLETE
1493
                )
1494
            );
1495
        }, 10);
1496
    }
1497
1498
    /**
1499
     * Check if all files (binaries table) for a collection are complete (if they all have the "parts").
1500
     * Set collections filecheck column to COLLFC_COMPPART.
1501
     * This means the collection is complete.
1502
     *
1503
     * @param string $where
1504
     *
1505
     * @void
1506
     * @throws \Throwable
1507
     */
1508
    private function collectionFileCheckStage4(&$where): void
1509
    {
1510
        DB::transaction(function () use ($where) {
1511
            DB::update(
1512
                sprintf(
1513
                    '
1514
				UPDATE collections c INNER JOIN
1515
					(SELECT c.id FROM collections c
1516
					INNER JOIN binaries b ON c.id = b.collections_id
1517
					WHERE b.partcheck = 1 AND c.filecheck IN (%d, %d) %s
1518
					GROUP BY b.collections_id, c.totalfiles, c.id HAVING COUNT(b.id) >= c.totalfiles)
1519
				r ON c.id = r.id SET filecheck = %d',
1520
                    self::COLLFC_TEMPCOMP,
1521
                    self::COLLFC_ZEROPART,
1522
                    $where,
1523
                    self::COLLFC_COMPPART
1524
                )
1525
            );
1526
        }, 10);
1527
    }
1528
1529
    /**
1530
     * If not all files (binaries table) had their parts on the previous stage,
1531
     * reset the collection filecheck column to COLLFC_COMPCOLL so we reprocess them next time.
1532
     *
1533
     * @param string $where
1534
     *
1535
     * @void
1536
     * @throws \Throwable
1537
     */
1538
    private function collectionFileCheckStage5(&$where): void
1539
    {
1540
        DB::transaction(function () use ($where) {
1541
            DB::update(
1542
                sprintf(
1543
                    '
1544
				UPDATE collections c
1545
				SET filecheck = %d
1546
				WHERE filecheck IN (%d, %d) %s',
1547
                    self::COLLFC_COMPCOLL,
1548
                    self::COLLFC_TEMPCOMP,
1549
                    self::COLLFC_ZEROPART,
1550
                    $where
1551
                )
1552
            );
1553
        }, 10);
1554
    }
1555
1556
    /**
1557
     * If a collection did not have the file count (ie: [00/12]) or the collection is incomplete after
1558
     * $this->collectionDelayTime hours, set the collection to complete to create it into a release/nzb.
1559
     *
1560
     * @param string $where
1561
     *
1562
     * @void
1563
     * @throws \Throwable
1564
     */
1565
    private function collectionFileCheckStage6(&$where): void
1566
    {
1567
        DB::transaction(function () use ($where) {
1568
            DB::update(
1569
                sprintf(
1570
                    "
1571
				UPDATE collections c SET filecheck = %d, totalfiles = (SELECT COUNT(b.id) FROM binaries b WHERE b.collections_id = c.id)
1572
				WHERE c.dateadded < NOW() - INTERVAL '%d' HOUR
1573
				AND c.filecheck IN (%d, %d, 10) %s",
1574
                    self::COLLFC_COMPPART,
1575
                    $this->collectionDelayTime,
1576
                    self::COLLFC_DEFAULT,
1577
                    self::COLLFC_COMPCOLL,
1578
                    $where
1579
                )
1580
            );
1581
        }, 10);
1582
    }
1583
1584
    /**
1585
     * If a collection has been stuck for $this->collectionTimeout hours, delete it, it's bad.
1586
     *
1587
     * @param string $where
1588
     *
1589
     * @void
1590
     * @throws \Exception
1591
     * @throws \Throwable
1592
     */
1593
    private function processStuckCollections($where): void
1594
    {
1595
        $lastRun = Settings::settingValue('indexer.processing.last_run_time');
1596
1597
        DB::transaction(function () use ($where, $lastRun) {
1598
            $obj = DB::delete(
1599
                sprintf(
1600
                    '
1601
                DELETE c FROM collections c
1602
                WHERE
1603
                    c.added <
1604
                    DATE_SUB(%s, INTERVAL %d HOUR)
1605
                %s',
1606
                    escapeString($lastRun),
1607
                    $this->collectionTimeout,
1608
                    $where
1609
                )
1610
            );
1611
            if ($this->echoCLI && $obj > 0) {
1612
                $this->colorCli->primary('Deleted '.$obj.' broken/stuck collections.', true);
1613
            }
1614
        }, 3);
1615
    }
1616
}
1617