Completed
Push — dev ( 16a6d5...47c158 )
by Darko
09:44
created

ProcessReleases::deleteCollections()   B

Complexity

Conditions 11
Paths 2

Size

Total Lines 81
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 0
Metric Value
eloc 47
dl 0
loc 81
ccs 0
cts 37
cp 0
rs 7.3166
c 0
b 0
f 0
cc 11
nc 2
nop 0
crap 132

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

570
                                $xrefGrpID = UsenetGroup::getIDByName(/** @scrutinizer ignore-type */ $grpTmp);
Loading history...
571
                                if ($xrefGrpID === '') {
572
                                    $xrefGrpID = UsenetGroup::addGroup(
573
                                        [
574
                                                'name'                  => $grpTmp,
575
                                                'description'           => 'Added by Release processing',
576
                                                'backfill_target'       => 1,
577
                                                'first_record'          => 0,
578
                                                'last_record'           => 0,
579
                                                'active'                => 0,
580
                                                'backfill'              => 0,
581
                                                'minfilestoformrelease' => '',
582
                                                'minsizetoformrelease'  => '',
583
                                            ]
584
                                        );
585
                                }
586
587
                                $relGroupsChk = ReleasesGroups::query()->where(
588
                                    [
589
                                            ['releases_id', '=', $releaseID],
590
                                            ['groups_id', '=', $xrefGrpID],
591
                                        ]
592
                                    )->first();
593
594
                                if ($relGroupsChk === null) {
595
                                    ReleasesGroups::query()->insert(
596
                                        [
597
                                                'releases_id' => $releaseID,
598
                                                'groups_id'   => $xrefGrpID,
599
                                            ]
600
                                        );
601
                                }
602
                            }
603
                        }
604
                    }
605
606
                    $returnCount++;
607
608
                    if ($this->echoCLI) {
609
                        echo "Added $returnCount releases.\r";
610
                    }
611
                }
612
            } else {
613
                // The release was already in the DB, so delete the collection.
614
                DB::transaction(function () use ($collection) {
615
                    Collection::query()->where('collectionhash', $collection->collectionhash)->delete();
616
                }, 3);
617
618
                $duplicate++;
619
            }
620
        }
621
622
        $totalTime = now()->diffInSeconds($startTime);
623
624
        if ($this->echoCLI) {
625
            $this->colorCli->primary(
626
                PHP_EOL.
627
                    number_format($returnCount).
628
                    ' Releases added and '.
629
                    number_format($duplicate).
630
                    ' duplicate collections deleted in '.
631
                    $totalTime.Str::plural(' second', $totalTime),
632
                true
633
                );
634
        }
635
636
        return ['added' => $returnCount, 'dupes' => $duplicate];
637
    }
638
639
    /**
640
     * Create NZB files from complete releases.
641
     *
642
     *
643
     * @return int
644
     * @throws \Throwable
645
     */
646
    public function createNZBs(): int
647
    {
648
        $startTime = now();
649
650
        if ($this->echoCLI) {
651
            $this->colorCli->header('Process Releases -> Create the NZB, delete collections/binaries/parts.');
652
        }
653
654
        $releases = Release::fromQuery(
655
            sprintf(
656
                "
657
				SELECT SQL_NO_CACHE
658
					CONCAT(COALESCE(cp.title,'') , CASE WHEN cp.title IS NULL THEN '' ELSE ' > ' END , c.title) AS title,
659
					r.name, r.id, r.guid
660
				FROM releases r
661
				INNER JOIN categories c ON r.categories_id = c.id
662
				INNER JOIN categories cp ON cp.id = c.parentid
663
				WHERE nzbstatus = 0"
664
            )
665
        );
666
667
        $nzbCount = 0;
668
669
        if (\count($releases) > 0) {
670
            $total = \count($releases);
671
            // Init vars for writing the NZB's.
672
            $this->nzb->initiateForWrite();
673
            foreach ($releases as $release) {
674
                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...
675
                    $nzbCount++;
676
                    if ($this->echoCLI) {
677
                        echo "Creating NZBs and deleting Collections: $nzbCount/$total.\r";
678
                    }
679
                }
680
            }
681
        }
682
683
        $totalTime = now()->diffInSeconds($startTime);
684
685
        if ($this->echoCLI) {
686
            $this->colorCli->primary(
687
                number_format($nzbCount).' NZBs created/Collections deleted in '.
688
                    $totalTime.Str::plural(' second', $totalTime).PHP_EOL.
689
                    'Total time: '.$totalTime.Str::plural(' second', $totalTime),
690
                true
691
            );
692
        }
693
694
        return $nzbCount;
695
    }
696
697
    /**
698
     * Categorize releases.
699
     *
700
     * @param int        $categorize
701
     * @param int|string $groupID (optional)
702
     *
703
     * @void
704
     * @throws \Exception
705
     */
706
    public function categorizeReleases($categorize, $groupID = ''): void
707
    {
708
        $startTime = now();
709
        if ($this->echoCLI) {
710
            $this->colorCli->header('Process Releases -> Categorize releases.');
711
        }
712
        switch ((int) $categorize) {
713
            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...
714
                $type = 'searchname';
715
                break;
716
            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...
717
            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...
718
719
                $type = 'name';
720
                break;
721
        }
722
        $this->categorizeRelease(
723
            $type,
724
            (! empty($groupID)
725
                ? 'WHERE categories_id = '.Category::OTHER_MISC.' AND iscategorized = 0 AND groups_id = '.$groupID
726
                : 'WHERE categories_id = '.Category::OTHER_MISC.' AND iscategorized = 0')
727
        );
728
729
        $totalTime = now()->diffInSeconds($startTime);
730
731
        if ($this->echoCLI) {
732
            $this->colorCli->primary($totalTime.Str::plural(' second', $totalTime));
733
        }
734
    }
735
736
    /**
737
     * Post-process releases.
738
     *
739
     * @param int  $postProcess
740
     * @param NNTP $nntp
741
     *
742
     * @void
743
     * @throws \Exception
744
     */
745
    public function postProcessReleases($postProcess, &$nntp): void
746
    {
747
        if ((int) $postProcess === 1) {
748
            (new PostProcess(['Echo' => $this->echoCLI]))->processAll($nntp);
749
        } elseif ($this->echoCLI) {
750
            $this->colorCli->info(
751
                'Post-processing is not running inside the Process Releases class.'.PHP_EOL.
752
                    'If you are using tmux or screen they might have their own scripts running Post-processing.'
753
                );
754
        }
755
    }
756
757
    /**
758
     * @param $groupID
759
     *
760
     * @throws \Exception
761
     * @throws \Throwable
762
     */
763
    public function deleteCollections(): void
764
    {
765
        $startTime = now();
766
767
        $deletedCount = 0;
768
769
        // CBP older than retention.
770
        if ($this->echoCLI) {
771
            echo
772
                $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

772
                /** @scrutinizer ignore-type */ $this->colorCli->header('Process Releases -> Delete finished collections.'.PHP_EOL).
Loading history...
773
                $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...
774
                    'Deleting collections/binaries/parts older than %d hours.',
775
                    Settings::settingValue('..partretentionhours')
776
                ), true);
777
        }
778
779
        DB::transaction(function () use ($deletedCount, $startTime) {
780
            $deleted = 0;
781
            $deleteQuery = Collection::query()
782
                ->where('dateadded', '<', now()->subHours(Settings::settingValue('..partretentionhours')))
0 ignored issues
show
Bug introduced by
App\Models\Settings::set...'..partretentionhours') of type App\Models\Settings is incompatible with the type integer expected by parameter $value of Carbon\Carbon::subHours(). ( Ignorable by Annotation )

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

782
                ->where('dateadded', '<', now()->subHours(/** @scrutinizer ignore-type */ Settings::settingValue('..partretentionhours')))
Loading history...
783
                ->delete();
784
            if ($deleteQuery > 0) {
785
                $deleted = $deleteQuery;
786
                $deletedCount += $deleted;
787
            }
788
            $firstQuery = $fourthQuery = now();
789
790
            $totalTime = $firstQuery->diffInSeconds($startTime);
791
792
            if ($this->echoCLI) {
793
                $this->colorCli->primary(
794
                    'Finished deleting '.$deleted.' old collections/binaries/parts in '.
795
                    $totalTime.Str::plural(' second', $totalTime),
796
                    true
797
                );
798
            }
799
800
            // Cleanup orphaned collections, binaries and parts
801
            // this really shouldn't happen, but just incase - so we only run 1/200 of the time
802
            if (random_int(0, 200) <= 1) {
803
                // CBP collection orphaned with no binaries or parts.
804
                if ($this->echoCLI) {
805
                    echo $this->colorCli->header('Process Releases -> Remove CBP orphans.'.PHP_EOL).$this->colorCli->primary('Deleting orphaned collections.');
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

805
                    echo /** @scrutinizer ignore-type */ $this->colorCli->header('Process Releases -> Remove CBP orphans.'.PHP_EOL).$this->colorCli->primary('Deleting orphaned collections.');
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...
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...
806
                }
807
808
                $deleted = 0;
809
                $deleteQuery = Collection::query()->whereNull('binaries.id')->orWhereNull('parts.binaries_id')->leftJoin('binaries', 'collections.id', '=', 'binaries.collections_id')->leftJoin('parts', 'binaries.id', '=', 'parts.binaries_id')->delete();
810
811
                if ($deleteQuery > 0) {
812
                    $deleted = $deleteQuery;
813
                    $deletedCount += $deleted;
814
                }
815
816
                $totalTime = now()->diffInSeconds($firstQuery);
817
818
                if ($this->echoCLI) {
819
                    $this->colorCli->primary('Finished deleting '.$deleted.' orphaned collections in '.$totalTime.Str::plural(' second', $totalTime), true);
820
                }
821
            }
822
823
            if ($this->echoCLI) {
824
                $this->colorCli->primary('Deleting collections that were missed after NZB creation.', true);
825
            }
826
827
            $deleted = 0;
828
            // Collections that were missing on NZB creation.
829
            $collections = Collection::query()->where('releases.nzbstatus', '=', 1)->leftJoin('releases', 'releases.id', '=', 'collections.releases_id')->select('collections.id')->get();
830
831
            foreach ($collections as $collection) {
832
                $deleted++;
833
                Collection::query()->where('id', $collection->id)->delete();
834
            }
835
            $deletedCount += $deleted;
836
837
            $colDelTime = now()->diffInSeconds($fourthQuery);
838
            $totalTime = $fourthQuery->diffInSeconds($startTime);
839
840
            if ($this->echoCLI) {
841
                $this->colorCli->primary('Finished deleting '.$deleted.' collections missed after NZB creation in '.$colDelTime.Str::plural(' second', $colDelTime).PHP_EOL.'Removed '.number_format($deletedCount).' parts/binaries/collection rows in '.$totalTime.Str::plural(' second', $totalTime), true);
842
            }
843
        }, 3);
844
    }
845
846
    /**
847
     * Delete unwanted releases based on admin settings.
848
     * This deletes releases based on group.
849
     *
850
     * @param int|string $groupID (optional)
851
     *
852
     * @void
853
     * @throws \Exception
854
     */
855
    public function deletedReleasesByGroup($groupID = ''): void
856
    {
857
        $startTime = now();
858
        $minSizeDeleted = $maxSizeDeleted = $minFilesDeleted = 0;
859
860
        if ($this->echoCLI) {
861
            $this->colorCli->header('Process Releases -> Delete releases smaller/larger than minimum size/file count from group/site setting.');
862
        }
863
864
        $groupID === '' ? $groupIDs = UsenetGroup::getActiveIDs() : $groupIDs = [['id' => $groupID]];
865
866
        $maxSizeSetting = Settings::settingValue('.release.maxsizetoformrelease');
867
        $minSizeSetting = Settings::settingValue('.release.minsizetoformrelease');
868
        $minFilesSetting = Settings::settingValue('.release.minfilestoformrelease');
869
870
        foreach ($groupIDs as $grpID) {
871
            $releases = Release::fromQuery(
872
                sprintf(
873
                    '
874
					SELECT SQL_NO_CACHE r.guid, r.id
875
					FROM releases r
876
					INNER JOIN usenet_groups g ON g.id = r.groups_id
877
					WHERE r.groups_id = %d
878
					AND greatest(IFNULL(g.minsizetoformrelease, 0), %d) > 0
879
					AND r.size < greatest(IFNULL(g.minsizetoformrelease, 0), %d)',
880
                    $grpID['id'],
881
                    $minSizeSetting,
882
                    $minSizeSetting
883
                )
884
            );
885
            foreach ($releases as $release) {
886
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
887
                $minSizeDeleted++;
888
            }
889
890
            if ($maxSizeSetting > 0) {
891
                $releases = Release::fromQuery(
892
                    sprintf(
893
                        '
894
						SELECT SQL_NO_CACHE id, guid
895
						FROM releases
896
						WHERE groups_id = %d
897
						AND size > %d',
898
                        $grpID['id'],
899
                        $maxSizeSetting
900
                    )
901
                );
902
                foreach ($releases as $release) {
903
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
904
                    $maxSizeDeleted++;
905
                }
906
            }
907
            if ($minFilesSetting > 0) {
908
                $releases = Release::fromQuery(
909
                    sprintf(
910
                        '
911
				SELECT SQL_NO_CACHE r.id, r.guid
912
				FROM releases r
913
				INNER JOIN usenet_groups g ON g.id = r.groups_id
914
				WHERE r.groups_id = %d
915
				AND greatest(IFNULL(g.minfilestoformrelease, 0), %d) > 0
916
				AND r.totalpart < greatest(IFNULL(g.minfilestoformrelease, 0), %d)',
917
                        $grpID['id'],
918
                        $minFilesSetting,
919
                        $minFilesSetting
920
                     )
921
                 );
922
                foreach ($releases as $release) {
923
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
924
                    $minFilesDeleted++;
925
                }
926
            }
927
        }
928
929
        $totalTime = now()->diffInSeconds($startTime);
930
931
        if ($this->echoCLI) {
932
            $this->colorCli->primary(
933
                'Deleted '.($minSizeDeleted + $maxSizeDeleted + $minFilesDeleted).
934
                    ' releases: '.PHP_EOL.
935
                    $minSizeDeleted.' smaller than, '.$maxSizeDeleted.' bigger than, '.$minFilesDeleted.
936
                    ' with less files than site/groups setting in: '.
937
                    $totalTime.Str::plural(' second', $totalTime),
938
                true
939
                );
940
        }
941
    }
942
943
    /**
944
     * Delete releases using admin settings.
945
     * This deletes releases, regardless of group.
946
     *
947
     * @void
948
     * @throws \Exception
949
     */
950
    public function deleteReleases(): void
951
    {
952
        $startTime = now();
953
        $genres = new Genres();
954
        $passwordDeleted = $duplicateDeleted = $retentionDeleted = $completionDeleted = $disabledCategoryDeleted = 0;
955
        $disabledGenreDeleted = $miscRetentionDeleted = $miscHashedDeleted = $categoryMinSizeDeleted = 0;
956
957
        // Delete old releases and finished collections.
958
        if ($this->echoCLI) {
959
            $this->colorCli->header('Process Releases -> Delete old releases and passworded releases.');
960
        }
961
962
        // Releases past retention.
963
        if ((int) Settings::settingValue('..releaseretentiondays') !== 0) {
964
            $releases = Release::fromQuery(
965
                sprintf(
966
                    'SELECT id, guid FROM releases WHERE postdate < (NOW() - INTERVAL %d DAY)',
967
                    (int) Settings::settingValue('..releaseretentiondays')
968
                )
969
            );
970
            foreach ($releases as $release) {
971
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
972
                $retentionDeleted++;
973
            }
974
        }
975
976
        // Passworded releases.
977
        if ((int) Settings::settingValue('..deletepasswordedrelease') === 1) {
978
            $releases = Release::fromQuery(
979
                sprintf(
980
                    'SELECT id, guid FROM releases WHERE passwordstatus = %d',
981
                    Releases::PASSWD_RAR
982
                )
983
            );
984
            foreach ($releases as $release) {
985
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
986
                $passwordDeleted++;
987
            }
988
        }
989
990
        // Possibly passworded releases.
991
        if ((int) Settings::settingValue('..deletepossiblerelease') === 1) {
992
            $releases = Release::fromQuery(
993
                sprintf(
994
                    'SELECT id, guid FROM releases WHERE passwordstatus = %d',
995
                    Releases::PASSWD_POTENTIAL
996
                )
997
            );
998
            foreach ($releases as $release) {
999
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1000
                $passwordDeleted++;
1001
            }
1002
        }
1003
1004
        if ((int) $this->crossPostTime !== 0) {
1005
            // Crossposted releases.
1006
            $releases = Release::fromQuery(
1007
                sprintf(
1008
                    'SELECT id, guid FROM releases WHERE adddate > (NOW() - INTERVAL %d HOUR) GROUP BY name HAVING COUNT(name) > 1',
1009
                    $this->crossPostTime
1010
                )
1011
            );
1012
            foreach ($releases as $release) {
1013
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1014
                $duplicateDeleted++;
1015
            }
1016
        }
1017
1018
        if ($this->completion > 0) {
1019
            $releases = Release::fromQuery(
1020
                sprintf('SELECT id, guid FROM releases WHERE completion < %d AND completion > 0', $this->completion)
1021
            );
1022
            foreach ($releases as $release) {
1023
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1024
                $completionDeleted++;
1025
            }
1026
        }
1027
1028
        // Disabled categories.
1029
        $disabledCategories = Category::getDisabledIDs();
1030
        if (\count($disabledCategories) > 0) {
1031
            foreach ($disabledCategories as $disabledCategory) {
1032
                $releases = Release::fromQuery(
1033
                    sprintf('SELECT id, guid FROM releases WHERE categories_id = %d', (int) $disabledCategory['id'])
1034
                );
1035
                foreach ($releases as $release) {
1036
                    $disabledCategoryDeleted++;
1037
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1038
                }
1039
            }
1040
        }
1041
1042
        // Delete smaller than category minimum sizes.
1043
        $categories = Category::fromQuery(
1044
            '
1045
			SELECT SQL_NO_CACHE c.id AS id,
1046
			CASE WHEN c.minsizetoformrelease = 0 THEN cp.minsizetoformrelease ELSE c.minsizetoformrelease END AS minsize
1047
			FROM categories c
1048
			INNER JOIN categories cp ON cp.id = c.parentid
1049
			WHERE c.parentid IS NOT NULL'
1050
        );
1051
1052
        foreach ($categories as $category) {
1053
            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...
1054
                $releases = Release::fromQuery(
1055
                    sprintf(
1056
                        '
1057
							SELECT id, guid
1058
							FROM releases
1059
							WHERE categories_id = %d
1060
							AND size < %d
1061
							LIMIT 1000',
1062
                        (int) $category->id,
1063
                        (int) $category->minsize
1064
                        )
1065
                    );
1066
                foreach ($releases as $release) {
1067
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1068
                    $categoryMinSizeDeleted++;
1069
                }
1070
            }
1071
        }
1072
1073
        // Disabled music genres.
1074
        $genrelist = $genres->getDisabledIDs();
1075
        if (\count($genrelist) > 0) {
1076
            foreach ($genrelist as $genre) {
1077
                $releases = Release::fromQuery(
1078
                    sprintf(
1079
                        '
1080
						SELECT id, guid
1081
						FROM releases r
1082
						INNER JOIN
1083
						(
1084
							SELECT id AS mid
1085
							FROM musicinfo
1086
							WHERE musicinfo.genre_id = %d
1087
						) mi ON musicinfo_id = mi.mid',
1088
                        (int) $genre['id']
1089
                    )
1090
                );
1091
                foreach ($releases as $release) {
1092
                    $disabledGenreDeleted++;
1093
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1094
                }
1095
            }
1096
        }
1097
1098
        // Misc other.
1099
        if (Settings::settingValue('..miscotherretentionhours') > 0) {
1100
            $releases = Release::fromQuery(
1101
                sprintf(
1102
                    '
1103
					SELECT SQL_NO_CACHE id, guid
1104
					FROM releases
1105
					WHERE categories_id = %d
1106
					AND adddate <= NOW() - INTERVAL %d HOUR',
1107
                    Category::OTHER_MISC,
1108
                    (int) Settings::settingValue('..miscotherretentionhours')
1109
                )
1110
            );
1111
            foreach ($releases as $release) {
1112
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1113
                $miscRetentionDeleted++;
1114
            }
1115
        }
1116
1117
        // Misc hashed.
1118
        if ((int) Settings::settingValue('..mischashedretentionhours') > 0) {
1119
            $releases = Release::fromQuery(
1120
                sprintf(
1121
                    '
1122
					SELECT SQL_NO_CACHE id, guid
1123
					FROM releases
1124
					WHERE categories_id = %d
1125
					AND adddate <= NOW() - INTERVAL %d HOUR',
1126
                    Category::OTHER_HASHED,
1127
                    (int) Settings::settingValue('..mischashedretentionhours')
1128
                )
1129
            );
1130
            foreach ($releases as $release) {
1131
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
1132
                $miscHashedDeleted++;
1133
            }
1134
        }
1135
1136
        if ($this->echoCLI) {
1137
            $this->colorCli->primary(
1138
                'Removed releases: '.
1139
                    number_format($retentionDeleted).
1140
                    ' past retention, '.
1141
                    number_format($passwordDeleted).
1142
                    ' passworded, '.
1143
                    number_format($duplicateDeleted).
1144
                    ' crossposted, '.
1145
                    number_format($disabledCategoryDeleted).
1146
                    ' from disabled categories, '.
1147
                    number_format($categoryMinSizeDeleted).
1148
                    ' smaller than category settings, '.
1149
                    number_format($disabledGenreDeleted).
1150
                    ' from disabled music genres, '.
1151
                    number_format($miscRetentionDeleted).
1152
                    ' from misc->other '.
1153
                    number_format($miscHashedDeleted).
1154
                    ' from misc->hashed'.
1155
                    (
1156
                        $this->completion > 0
1157
                        ? ', '.number_format($completionDeleted).' under '.$this->completion.'% completion.'
1158
                        : '.'
1159
                    ),
1160
                true
1161
                );
1162
1163
            $totalDeleted = (
1164
                $retentionDeleted + $passwordDeleted + $duplicateDeleted + $disabledCategoryDeleted +
1165
                $disabledGenreDeleted + $miscRetentionDeleted + $miscHashedDeleted + $completionDeleted +
1166
                $categoryMinSizeDeleted
1167
            );
1168
            if ($totalDeleted > 0) {
1169
                $totalTime = now()->diffInSeconds($startTime);
1170
                $this->colorCli->primary(
1171
                    'Removed '.number_format($totalDeleted).' releases in '.
1172
                        $totalTime.Str::plural(' second', $totalTime),
1173
                    true
1174
                    );
1175
            }
1176
        }
1177
    }
1178
1179
    /**
1180
     * Look if we have all the files in a collection (which have the file count in the subject).
1181
     * Set file check to complete.
1182
     * This means the the binary table has the same count as the file count in the subject, but
1183
     * the collection might not be complete yet since we might not have all the articles in the parts table.
1184
     *
1185
     *
1186
     * @void
1187
     * @throws \Throwable
1188
     */
1189
    private function collectionFileCheckStage1(): void
1190
    {
1191
        DB::transaction(function () {
1192
            DB::update(
1193
                sprintf(
1194
                    '
1195
				UPDATE collections c
1196
				INNER JOIN
1197
				(
1198
					SELECT c.id
1199
					FROM collections c
1200
					INNER JOIN binaries b ON b.collections_id = c.id
1201
					WHERE c.totalfiles > 0
1202
					AND c.filecheck = %d
1203
					GROUP BY b.collections_id, c.totalfiles, c.id
1204
					HAVING COUNT(b.id) IN (c.totalfiles, c.totalfiles + 1)
1205
				) r ON c.id = r.id
1206
				SET filecheck = %d',
1207
                    self::COLLFC_DEFAULT,
1208
                    self::COLLFC_COMPCOLL
1209
                )
1210
            );
1211
        }, 3);
1212
    }
1213
1214
    /**
1215
     * The first query sets filecheck to COLLFC_ZEROPART if there's a file that starts with 0 (ex. [00/100]).
1216
     * The second query sets filecheck to COLLFC_TEMPCOMP on everything left over, so anything that starts with 1 (ex. [01/100]).
1217
     *
1218
     * This is done because some collections start at 0 and some at 1, so if you were to assume the collection is complete
1219
     * at 0 then you would never get a complete collection if it starts with 1 and if it starts, you can end up creating
1220
     * a incomplete collection, since you assumed it was complete.
1221
     *
1222
     *
1223
     * @void
1224
     * @throws \Throwable
1225
     */
1226
    private function collectionFileCheckStage2(): void
1227
    {
1228
        DB::transaction(function () {
1229
            DB::update(
1230
                sprintf(
1231
                    '
1232
				UPDATE collections c
1233
				INNER JOIN
1234
				(
1235
					SELECT c.id
1236
					FROM collections c
1237
					INNER JOIN binaries b ON b.collections_id = c.id
1238
					WHERE b.filenumber = 0
1239
					AND c.totalfiles > 0
1240
					AND c.filecheck = %d
1241
					GROUP BY c.id
1242
				) r ON c.id = r.id
1243
				SET c.filecheck = %d',
1244
                    self::COLLFC_COMPCOLL,
1245
                    self::COLLFC_ZEROPART
1246
                )
1247
            );
1248
        }, 3);
1249
1250
        DB::transaction(function () {
1251
            Collection::query()->where('filecheck', self::COLLFC_COMPCOLL)->update(['filecheck' => self::COLLFC_TEMPCOMP]);
1252
        }, 3);
1253
    }
1254
1255
    /**
1256
     * Check if the files (binaries table) in a complete collection has all the parts.
1257
     * If we have all the parts, set binaries table partcheck to FILE_COMPLETE.
1258
     *
1259
     *
1260
     * @void
1261
     * @throws \Throwable
1262
     */
1263
    private function collectionFileCheckStage3(): void
1264
    {
1265
        DB::transaction(function () {
1266
            DB::update(
1267
                sprintf(
1268
                    '
1269
				UPDATE binaries b
1270
				INNER JOIN
1271
				(
1272
					SELECT b.id
1273
					FROM binaries b
1274
					INNER JOIN collections c ON c.id = b.collections_id
1275
					WHERE c.filecheck = %d
1276
					AND b.partcheck = %d
1277
					AND b.currentparts = b.totalparts
1278
					GROUP BY b.id, b.totalparts
1279
				) r ON b.id = r.id
1280
				SET b.partcheck = %d',
1281
                    self::COLLFC_TEMPCOMP,
1282
                    self::FILE_INCOMPLETE,
1283
                    self::FILE_COMPLETE
1284
            )
1285
        );
1286
        }, 3);
1287
1288
        DB::transaction(function () {
1289
            DB::update(
1290
                sprintf(
1291
                    '
1292
				UPDATE binaries b
1293
				INNER JOIN
1294
				(
1295
					SELECT b.id
1296
					FROM binaries b
1297
					INNER JOIN collections c ON c.id = b.collections_id
1298
					WHERE c.filecheck = %d
1299
					AND b.partcheck = %d
1300
					AND b.currentparts >= (b.totalparts + 1)
1301
					GROUP BY b.id, b.totalparts
1302
				) r ON b.id = r.id
1303
				SET b.partcheck = %d',
1304
                    self::COLLFC_ZEROPART,
1305
                    self::FILE_INCOMPLETE,
1306
                    self::FILE_COMPLETE
1307
                )
1308
            );
1309
        }, 3);
1310
    }
1311
1312
    /**
1313
     * Check if all files (binaries table) for a collection are complete (if they all have the "parts").
1314
     * Set collections filecheck column to COLLFC_COMPPART.
1315
     * This means the collection is complete.
1316
     *
1317
     *
1318
     * @void
1319
     * @throws \Throwable
1320
     */
1321
    private function collectionFileCheckStage4(): void
1322
    {
1323
        DB::transaction(function () {
1324
            DB::update(
1325
                sprintf(
1326
                    '
1327
				UPDATE collections c INNER JOIN
1328
					(SELECT c.id FROM collections c
1329
					INNER JOIN binaries b ON c.id = b.collections_id
1330
					WHERE b.partcheck = 1 AND c.filecheck IN (%d, %d)
1331
					GROUP BY b.collections_id, c.totalfiles, c.id HAVING COUNT(b.id) >= c.totalfiles)
1332
				r ON c.id = r.id SET filecheck = %d',
1333
                    self::COLLFC_TEMPCOMP,
1334
                    self::COLLFC_ZEROPART,
1335
                    self::COLLFC_COMPPART
1336
                )
1337
            );
1338
        }, 3);
1339
    }
1340
1341
    /**
1342
     * If not all files (binaries table) had their parts on the previous stage,
1343
     * reset the collection filecheck column to COLLFC_COMPCOLL so we reprocess them next time.
1344
     *
1345
     *
1346
     * @void
1347
     * @throws \Throwable
1348
     */
1349
    private function collectionFileCheckStage5(): void
1350
    {
1351
        DB::transaction(function () {
1352
            DB::update(
1353
                sprintf(
1354
                    '
1355
				UPDATE collections c
1356
				SET filecheck = %d
1357
				WHERE filecheck IN (%d, %d)',
1358
                    self::COLLFC_COMPCOLL,
1359
                    self::COLLFC_TEMPCOMP,
1360
                    self::COLLFC_ZEROPART
1361
                )
1362
            );
1363
        }, 3);
1364
    }
1365
1366
    /**
1367
     * If a collection did not have the file count (ie: [00/12]) or the collection is incomplete after
1368
     * $this->collectionDelayTime hours, set the collection to complete to create it into a release/nzb.
1369
     *
1370
     *
1371
     * @void
1372
     * @throws \Throwable
1373
     */
1374
    private function collectionFileCheckStage6(): void
1375
    {
1376
        DB::transaction(function () {
1377
            DB::update(
1378
                sprintf(
1379
                    "
1380
				UPDATE collections c SET filecheck = %d, totalfiles = (SELECT COUNT(b.id) FROM binaries b WHERE b.collections_id = c.id)
1381
				WHERE c.dateadded < NOW() - INTERVAL '%d' HOUR
1382
				AND c.filecheck IN (%d, %d, 3)",
1383
                    self::COLLFC_COMPPART,
1384
                    $this->collectionDelayTime,
1385
                    self::COLLFC_DEFAULT,
1386
                    self::COLLFC_COMPCOLL
1387
                )
1388
            );
1389
        }, 3);
1390
    }
1391
1392
    /**
1393
     * If a collection has been stuck for $this->collectionTimeout hours, delete it, it's bad.
1394
     *
1395
     *
1396
     * @void
1397
     * @throws \Exception
1398
     * @throws \Throwable
1399
     */
1400
    private function processStuckCollections(): void
1401
    {
1402
        $lastRun = Settings::settingValue('indexer.processing.last_run_time');
1403
1404
        DB::transaction(function () use ($lastRun) {
1405
            $obj = DB::delete(
1406
                sprintf(
1407
                    '
1408
                DELETE c FROM collections c
1409
                WHERE
1410
                    c.added <
1411
                    DATE_SUB(%s, INTERVAL %d HOUR)',
1412
                    escapeString($lastRun),
1413
                    $this->collectionTimeout
1414
                )
1415
            );
1416
            if ($this->echoCLI && $obj > 0) {
1417
                $this->colorCli->primary('Deleted '.$obj.' broken/stuck collections.', true);
1418
            }
1419
        }, 3);
1420
    }
1421
}
1422