Completed
Push — dev ( 9fc2d2...4254b8 )
by Darko
07:26
created

ProcessReleases::deletedReleasesByGroup()   B

Complexity

Conditions 10
Paths 104

Size

Total Lines 48
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
eloc 32
dl 0
loc 48
ccs 0
cts 24
cp 0
rs 7.6333
c 0
b 0
f 0
cc 10
nc 104
nop 1
crap 110

How to fix   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 App\Models\MusicInfo;
6
use Blacklight\NZB;
7
use Blacklight\NNTP;
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 App\Models\Collection;
16
use Blacklight\Categorize;
17
use App\Models\UsenetGroup;
18
use Illuminate\Support\Str;
19
use App\Models\ReleaseRegex;
20
use Blacklight\ConsoleTools;
21
use Blacklight\ReleaseImage;
22
use App\Models\ReleasesGroups;
23
use Illuminate\Support\Carbon;
24
use Blacklight\ReleaseCleaning;
25
use Illuminate\Support\Facades\DB;
26
use Illuminate\Support\Facades\Log;
27
28
class ProcessReleases
29
{
30
    public const COLLFC_DEFAULT = 0; // Collection has default filecheck status
31
    public const COLLFC_COMPCOLL = 1; // Collection is a complete collection
32
    public const COLLFC_COMPPART = 2; // Collection is a complete collection and has all parts available
33
    public const COLLFC_SIZED = 3; // Collection has been calculated for total size
34
    public const COLLFC_INSERTED = 4; // Collection has been inserted into releases
35
    public const COLLFC_DELETE = 5; // Collection is ready for deletion
36
    public const COLLFC_TEMPCOMP = 15; // Collection is complete and being checked for complete parts
37
    public const COLLFC_ZEROPART = 16; // Collection has a 00/0XX designator (temporary)
38
39
    public const FILE_INCOMPLETE = 0; // We don't have all the parts yet for the file (binaries table partcheck column).
40
    public const FILE_COMPLETE = 1; // We have all the parts for the file (binaries table partcheck column).
41
42
    /**
43
     * @var int
44
     */
45
    public $collectionDelayTime;
46
47
    /**
48
     * @var int
49
     */
50
    public $crossPostTime;
51
52
    /**
53
     * @var int
54
     */
55
    public $releaseCreationLimit;
56
57
    /**
58
     * @var int
59
     */
60
    public $completion;
61
62
    /**
63
     * @var bool
64
     */
65
    public $echoCLI;
66
67
    /**
68
     * @var \PDO
69
     */
70
    public $pdo;
71
72
    /**
73
     * @var \Blacklight\ConsoleTools
74
     */
75
    public $consoleTools;
76
77
    /**
78
     * @var \Blacklight\NZB
79
     */
80
    public $nzb;
81
82
    /**
83
     * @var \Blacklight\ReleaseCleaning
84
     */
85
    public $releaseCleaning;
86
87
    /**
88
     * @var \Blacklight\Releases
89
     */
90
    public $releases;
91
92
    /**
93
     * @var \Blacklight\ReleaseImage
94
     */
95
    public $releaseImage;
96
97
    /**
98
     * Time (hours) to wait before delete a stuck/broken collection.
99
     *
100
     *
101
     * @var int
102
     */
103
    private $collectionTimeout;
104
105
    /**
106
     * @var \Blacklight\ColorCLI
107
     */
108
    protected $colorCli;
109
110
    /**
111
     * @param array $options Class instances / Echo to cli ?
112
     *
113
     * @throws \Exception
114
     */
115
    public function __construct(array $options = [])
116
    {
117
        $defaults = [
118
            'Echo'            => true,
119
            'ConsoleTools'    => null,
120
            'Groups'          => null,
121
            'NZB'             => null,
122
            'ReleaseCleaning' => null,
123
            'ReleaseImage'    => null,
124
            'Releases'        => null,
125
            'Settings'        => null,
126
        ];
127
        $options += $defaults;
128
129
        $this->echoCLI = ($options['Echo'] && config('nntmux.echocli'));
130
131
        $this->consoleTools = ($options['ConsoleTools'] instanceof ConsoleTools ? $options['ConsoleTools'] : new ConsoleTools());
132
        $this->nzb = ($options['NZB'] instanceof NZB ? $options['NZB'] : new NZB());
133
        $this->releaseCleaning = ($options['ReleaseCleaning'] instanceof ReleaseCleaning ? $options['ReleaseCleaning'] : new ReleaseCleaning());
134
        $this->releases = ($options['Releases'] instanceof Releases ? $options['Releases'] : new Releases());
135
        $this->releaseImage = ($options['ReleaseImage'] instanceof ReleaseImage ? $options['ReleaseImage'] : new ReleaseImage());
136
        $this->colorCli = new ColorCLI();
137
138
        $dummy = Settings::settingValue('..delaytime');
139
        $this->collectionDelayTime = ($dummy !== '' ? (int) $dummy : 2);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
140
        $dummy = Settings::settingValue('..crossposttime');
141
        $this->crossPostTime = ($dummy !== '' ? (int) $dummy : 2);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
142
        $dummy = Settings::settingValue('..maxnzbsprocessed');
143
        $this->releaseCreationLimit = ($dummy !== '' ? (int) $dummy : 1000);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
144
        $dummy = Settings::settingValue('..completionpercent');
145
        $this->completion = ($dummy !== '' ? (int) $dummy : 0);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
146
        if ($this->completion > 100) {
147
            $this->completion = 100;
148
            $this->colorCli->error(PHP_EOL.'You have an invalid setting for completion. It cannot be higher than 100.');
149
        }
150
        $this->collectionTimeout = (int) Settings::settingValue('indexer.processing.collection_timeout');
151
    }
152
153
    /**
154
     * Main method for creating releases/NZB files from collections.
155
     *
156
     * @param int              $categorize
157
     * @param int              $postProcess
158
     * @param string           $groupName (optional)
159
     * @param \Blacklight\NNTP $nntp
160
     * @param bool             $echooutput
161
     *
162
     * @return int
163
     * @throws \Throwable
164
     */
165
    public function processReleases($categorize, $postProcess, $groupName, &$nntp, $echooutput): int
166
    {
167
        $this->echoCLI = ($echooutput && config('nntmux.echocli'));
168
        $groupID = '';
169
170
        if (! empty($groupName) && $groupName !== 'mgr') {
171
            $groupInfo = UsenetGroup::getByName($groupName);
172
            if ($groupInfo !== null) {
173
                $groupID = $groupInfo['id'];
174
            }
175
        }
176
177
        if ($this->echoCLI) {
178
            $this->colorCli->header('Starting release update process ('.now()->format('Y-m-d H:i:s').')');
179
        }
180
181
        if (! file_exists(Settings::settingValue('..nzbpath'))) {
182
            if ($this->echoCLI) {
183
                $this->colorCli->error('Bad or missing nzb directory - '.Settings::settingValue('..nzbpath'));
184
            }
185
186
            return 0;
187
        }
188
189
        $this->processIncompleteCollections($groupID);
190
        $this->processCollectionSizes($groupID);
191
        $this->deleteUnwantedCollections($groupID);
192
193
        $totalReleasesAdded = 0;
194
        do {
195
            $releasesCount = $this->createReleases($groupID);
196
            $totalReleasesAdded += $releasesCount['added'];
197
198
            $nzbFilesAdded = $this->createNZBs($groupID);
199
200
            $this->categorizeReleases($categorize, $groupID);
201
            $this->postProcessReleases($postProcess, $nntp);
202
            $this->deleteCollections($groupID);
203
204
            // This loops as long as the number of releases or nzbs added was >= the limit (meaning there are more waiting to be created)
205
        } while (
206
            ($releasesCount['added'] + $releasesCount['dupes']) >= $this->releaseCreationLimit
207
            || $nzbFilesAdded >= $this->releaseCreationLimit
208
        );
209
210
        // Only run if non-mgr as mgr is not specific to group
211
        if ($groupName !== 'mgr') {
212
            $this->deletedReleasesByGroup($groupID);
213
            $this->deleteReleases();
214
        }
215
216
        return $totalReleasesAdded;
217
    }
218
219
    /**
220
     * Return all releases to other->misc category.
221
     *
222
     * @param string $where Optional "where" query parameter.
223
     *
224
     * @void
225
     */
226
    public function resetCategorize($where = ''): void
227
    {
228
        DB::update(
229
            sprintf('UPDATE releases SET categories_id = %d, iscategorized = 0 %s', Category::OTHER_MISC, $where)
230
        );
231
    }
232
233
    /**
234
     * Categorizes releases.
235
     *
236
     * @param string $type name or searchname | Categorize using the search name or subject.
237
     * @param $groupId
238
     * @return int Quantity of categorized releases.
239
     * @throws \Exception
240
     */
241
    public function categorizeRelease($type, $groupId): int
242
    {
243
        $cat = new Categorize();
244
        $categorized = $total = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $total is dead and can be removed.
Loading history...
245
        $releasesQuery = Release::query()->where(['categories_id' => Category::OTHER_MISC, 'iscategorized' => 0]);
246
        if (! empty($groupId)) {
247
            $releasesQuery->where('groups_id', $groupId);
248
        }
249
        $releases = $releasesQuery->select(['id', 'fromname', 'groups_id', $type])->get();
250
        if ($releases->count() > 0) {
251
            $total = \count($releases);
252
            foreach ($releases as $release) {
253
                $catId = $cat->determineCategory($release->groups_id, $release->{$type}, $release->fromname);
254
                Release::query()->where('id', $release->id)->update(['categories_id' => $catId['categories_id'], 'iscategorized' => 1]);
255
                try {
256
                    $taggedRelease = Release::find($release->id);
257
                    $taggedRelease->retag($catId['tags']);
258
                } catch (\Throwable $e) {
259
                    //Just pass this part, tag is not created for some reason, exception is thrown and blocks release creation
260
                }
261
                $categorized++;
262
                if ($this->echoCLI) {
263
                    $this->consoleTools->overWritePrimary(
264
                        'Categorizing: '.$this->consoleTools->percentString($categorized, $total)
265
                    );
266
                }
267
            }
268
        }
269
        if ($this->echoCLI && $categorized > 0) {
270
            echo PHP_EOL;
271
        }
272
273
        return $categorized;
274
    }
275
276
    /**
277
     * @param $groupID
278
     *
279
     * @throws \Exception
280
     * @throws \Throwable
281
     */
282
    public function processIncompleteCollections($groupID): void
283
    {
284
        $startTime = now();
285
286
        if ($this->echoCLI) {
287
            $this->colorCli->header('Process Releases -> Attempting to find complete collections.');
288
        }
289
290
        $where = (! empty($groupID) ? ' AND c.groups_id = '.$groupID.' ' : ' ');
291
292
        $this->processStuckCollections($groupID);
293
        $this->collectionFileCheckStage1($groupID);
294
        $this->collectionFileCheckStage2($groupID);
295
        $this->collectionFileCheckStage3($where);
296
        $this->collectionFileCheckStage4($where);
297
        $this->collectionFileCheckStage5($groupID);
298
        $this->collectionFileCheckStage6($where);
299
300
        if ($this->echoCLI) {
301
            $countQuery = Collection::query()->where('filecheck', self::COLLFC_COMPPART);
302
303
            if (! empty($groupID)) {
304
                $countQuery->where('groups_id', $groupID);
305
            }
306
            $count = $countQuery->count('id');
307
308
            $totalTime = now()->diffInSeconds($startTime);
309
310
            $this->colorCli->primary(
311
                ($count ?? 0).' collections were found to be complete. Time: '.
0 ignored issues
show
Bug introduced by
Are you sure $count ?? 0 of type Illuminate\Database\Eloquent\Builder|integer|mixed 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

311
                (/** @scrutinizer ignore-type */ $count ?? 0).' collections were found to be complete. Time: '.
Loading history...
312
                $totalTime.Str::plural(' second', $totalTime),
313
                true
314
            );
315
        }
316
    }
317
318
    /**
319
     * @param $groupID
320
     *
321
     * @throws \Exception
322
     * @throws \Throwable
323
     */
324
    public function processCollectionSizes($groupID): 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 ($groupID, $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 %s',
346
                    self::COLLFC_SIZED,
347
                    self::COLLFC_COMPPART,
348
                    (! empty($groupID) ? ' AND c.groups_id = '.$groupID : ' ')
349
                )
350
            );
351
            if ($checked > 0 && $this->echoCLI) {
352
                $this->colorCli->primary(
353
                    $checked.' collections set to filecheck = 3(size calculated)',
354
                    true
355
                );
356
                $totalTime = now()->diffInSeconds($startTime);
357
                $this->colorCli->primary($totalTime.Str::plural(' second', $totalTime), true);
358
            }
359
        }, 10);
360
    }
361
362
    /**
363
     * @param $groupID
364
     *
365
     * @throws \Exception
366
     * @throws \Throwable
367
     */
368
    public function deleteUnwantedCollections($groupID): void
369
    {
370
        $startTime = now();
371
372
        if ($this->echoCLI) {
373
            $this->colorCli->header('Process Releases -> Delete collections smaller/larger than minimum size/file count from group/site setting.');
374
        }
375
376
        $groupID === '' ? $groupIDs = UsenetGroup::getActiveIDs() : $groupIDs = [['id' => $groupID]];
377
378
        $minSizeDeleted = $maxSizeDeleted = $minFilesDeleted = 0;
379
380
        $maxSizeSetting = Settings::settingValue('.release.maxsizetoformrelease');
381
        $minSizeSetting = Settings::settingValue('.release.minsizetoformrelease');
382
        $minFilesSetting = Settings::settingValue('.release.minfilestoformrelease');
383
384
        foreach ($groupIDs as $grpID) {
385
            $groupMinSizeSetting = $groupMinFilesSetting = 0;
386
387
            $groupMinimums = UsenetGroup::getGroupByID($grpID['id']);
388
            if ($groupMinimums !== null) {
389
                if (! empty($groupMinimums['minsizetoformrelease']) && $groupMinimums['minsizetoformrelease'] > 0) {
390
                    $groupMinSizeSetting = (int) $groupMinimums['minsizetoformrelease'];
391
                }
392
                if (! empty($groupMinimums['minfilestoformrelease']) && $groupMinimums['minfilestoformrelease'] > 0) {
393
                    $groupMinFilesSetting = (int) $groupMinimums['minfilestoformrelease'];
394
                }
395
            }
396
397
            if (Collection::query()->where('filecheck', self::COLLFC_SIZED)->where('filesize', '>', 0)->first() !== null) {
398
                DB::transaction(function () use (
399
                    $groupMinSizeSetting,
400
                    $minSizeSetting,
401
                    $minSizeDeleted,
402
                    $maxSizeSetting,
403
                    $maxSizeDeleted,
404
                    $minFilesSetting,
405
                    $groupMinFilesSetting,
406
                    $minFilesDeleted,
407
                    $startTime
408
                ) {
409
                    $deleteQuery = Collection::query()
410
                        ->where('filecheck', self::COLLFC_SIZED)
411
                        ->where('filesize', '>', 0)
412
                        ->whereRaw('GREATEST(?, ?) > 0 AND filesize < GREATEST(?, ?)', [$groupMinSizeSetting, $minSizeSetting, $groupMinSizeSetting, $minSizeSetting])
413
                        ->delete();
414
415
                    if ($deleteQuery > 0) {
416
                        $minSizeDeleted += $deleteQuery;
417
                    }
418
419
                    if ($maxSizeSetting > 0) {
420
                        $deleteQuery = Collection::query()
421
                            ->where('filecheck', '=', self::COLLFC_SIZED)
422
                            ->where('filesize', '>', $maxSizeSetting)
423
                            ->delete();
424
425
                        if ($deleteQuery > 0) {
426
                            $maxSizeDeleted += $deleteQuery;
427
                        }
428
                    }
429
430
                    if ($minFilesSetting > 0 || $groupMinFilesSetting > 0) {
431
                        $deleteQuery = Collection::query()
432
                            ->where('filecheck', self::COLLFC_SIZED)
433
                            ->where('filesize', '>', 0)
434
                            ->whereRaw('GREATEST(?, ?) > 0 AND totalfiles < GREATEST(?, ?)', [$groupMinFilesSetting, $minFilesSetting, $groupMinFilesSetting, $minFilesSetting])
435
                            ->delete();
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
                }, 10);
448
            }
449
        }
450
    }
451
452
    /**
453
     * @param int|string $groupID (optional)
454
     *
455
     * @return array
456
     * @throws \Throwable
457
     */
458
    public function createReleases($groupID): array
459
    {
460
        $startTime = now();
461
462
        $categorize = new Categorize();
463
        $returnCount = $duplicate = 0;
464
465
        if ($this->echoCLI) {
466
            $this->colorCli->header('Process Releases -> Create releases from complete collections.');
467
        }
468
        $collectionsQuery = Collection::query()
469
            ->where('collections.filecheck', self::COLLFC_SIZED)
470
            ->where('collections.filesize', '>', 0);
471
        if (! empty($groupID)) {
472
            $collectionsQuery->where('collections.groups_id', $groupID);
473
        }
474
        $collectionsQuery->select(['collections.*', 'usenet_groups.name as gname'])
475
            ->join('usenet_groups', 'usenet_groups.id', '=', 'collections.groups_id')
476
            ->limit($this->releaseCreationLimit);
477
        $collections = $collectionsQuery->get();
478
        if ($this->echoCLI && $collections->count() > 0) {
479
            $this->colorCli->primary(\count($collections).' Collections ready to be converted to releases.', true);
480
        }
481
482
        foreach ($collections as $collection) {
483
            $cleanRelName = utf8_encode(str_replace(['#', '@', '$', '%', '^', '§', '¨', '©', 'Ö'], '', $collection->subject));
484
            $fromName = utf8_encode(
485
                trim($collection->fromname, "'")
486
            );
487
488
            // Look for duplicates, duplicates match on releases.name, releases.fromname and releases.size
489
            // A 1% variance in size is considered the same size when the subject and poster are the same
490
            $dupeCheck = Release::query()
491
                ->where(['name' => $cleanRelName, 'fromname' => $fromName])
492
                ->whereBetween('size', [$collection->filesize * .99, $collection->filesize * 1.01])
493
                ->first(['id']);
494
495
            if ($dupeCheck === null) {
496
                $cleanedName = $this->releaseCleaning->releaseCleaner(
497
                    $collection->subject,
498
                    $collection->fromname,
499
                    $collection->gname
0 ignored issues
show
Bug introduced by
The property gname does not seem to exist on App\Models\Collection. 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...
500
                );
501
502
                if (\is_array($cleanedName)) {
503
                    $properName = $cleanedName['properlynamed'];
504
                    $preID = $cleanedName['predb'] ?? false;
505
                    $cleanedName = $cleanedName['cleansubject'];
506
                } else {
507
                    $properName = true;
508
                    $preID = false;
509
                }
510
511
                if ($preID === false && $cleanedName !== '') {
512
                    // try to match the cleaned searchname to predb title or filename here
513
                    $preMatch = Predb::matchPre($cleanedName);
514
                    if ($preMatch !== false) {
515
                        $cleanedName = $preMatch['title'];
516
                        $preID = $preMatch['predb_id'];
517
                        $properName = true;
518
                    }
519
                }
520
521
                $determinedCategory = $categorize->determineCategory($collection->groups_id, $cleanedName);
522
523
                $releaseID = Release::insertRelease(
524
                    [
525
                        'name' => $cleanRelName,
526
                        'searchname' => ! empty($cleanedName) ? utf8_encode($cleanedName) : $cleanRelName,
527
                        'totalpart' => $collection->totalfiles,
528
                        'groups_id' => $collection->groups_id,
529
                        'guid' => createGUID(),
530
                        'postdate' => $collection->date,
531
                        'fromname' => $fromName,
532
                        'size' => $collection->filesize,
533
                        'categories_id' => $determinedCategory['categories_id'],
534
                        'isrenamed' => $properName === true ? 1 : 0,
535
                        'predb_id' => $preID === false ? 0 : $preID,
536
                        'nzbstatus' => NZB::NZB_NONE,
537
                    ]
538
                );
539
                try {
540
                    $release = Release::find($releaseID);
541
                    $release->retag($determinedCategory['tags']);
542
                } catch (\Throwable $e) {
543
                    Log::debug($e->getTraceAsString());
544
                    //Just pass this part, tag is not created for some reason, exception is thrown and blocks release creation
545
                }
546
547
                if ($releaseID !== null) {
548
                    // Update collections table to say we inserted the release.
549
                    DB::transaction(function () use ($collection, $releaseID) {
550
                        Collection::query()->where('id', $collection->id)->update(['filecheck' => self::COLLFC_INSERTED, 'releases_id' => $releaseID]);
551
                    }, 10);
552
553
                    // Add the id of regex that matched the collection and release name to release_regexes table
554
                    ReleaseRegex::insertIgnore([
555
                        'releases_id'            => $releaseID,
556
                        'collection_regex_id'    => $collection->collection_regexes_id,
557
                        'naming_regex_id'        => $cleanedName['id'] ?? 0,
558
                    ]);
559
560
                    if (preg_match_all('#(\S+):\S+#', $collection->xref, $matches)) {
561
                        foreach ($matches[1] as $grp) {
562
                            //check if the group name is in a valid format
563
                            $grpTmp = UsenetGroup::isValidGroup($grp);
564
                            if ($grpTmp !== false) {
565
                                //check if the group already exists in database
566
                                $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

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

758
    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...
759
    {
760
        $startTime = now();
761
762
        $deletedCount = 0;
763
764
        // CBP older than retention.
765
        if ($this->echoCLI) {
766
            echo
767
                $this->colorCli->header('Process Releases -> Delete finished collections.'.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

767
                /** @scrutinizer ignore-type */ $this->colorCli->header('Process Releases -> Delete finished collections.'.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...
768
                $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...
769
                    'Deleting collections/binaries/parts older than %d hours.',
770
                    Settings::settingValue('..partretentionhours')
771
                ), true);
772
        }
773
774
        DB::transaction(function () use ($deletedCount, $startTime) {
775
            $deleted = 0;
776
            $deleteQuery = Collection::query()
777
                ->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

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

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