Passed
Push — master ( 0980e4...0c25e1 )
by Darko
16:40
created

ProcessReleases::deleteReleases()   F

Complexity

Conditions 26
Paths 6144

Size

Total Lines 146
Code Lines 95

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 95
dl 0
loc 146
rs 0
c 0
b 0
f 0
cc 26
nc 6144
nop 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace Blacklight\processing;
4
5
use App\Models\Category;
6
use App\Models\Collection;
7
use App\Models\MusicInfo;
8
use App\Models\Predb;
9
use App\Models\Release;
10
use App\Models\ReleaseRegex;
11
use App\Models\ReleasesGroups;
12
use App\Models\Settings;
13
use App\Models\UsenetGroup;
14
use Blacklight\Categorize;
15
use Blacklight\ColorCLI;
16
use Blacklight\Genres;
17
use Blacklight\NNTP;
18
use Blacklight\NZB;
19
use Blacklight\ReleaseCleaning;
20
use Blacklight\ReleaseImage;
21
use Blacklight\Releases;
22
use Illuminate\Support\Carbon;
23
use Illuminate\Support\Facades\DB;
24
use Illuminate\Support\Str;
25
use JetBrains\PhpStorm\ArrayShape;
26
27
class ProcessReleases
28
{
29
    public const COLLFC_DEFAULT = 0; // Collection has default filecheck status
30
31
    public const COLLFC_COMPCOLL = 1; // Collection is a complete collection
32
33
    public const COLLFC_COMPPART = 2; // Collection is a complete collection and has all parts available
34
35
    public const COLLFC_SIZED = 3; // Collection has been calculated for total size
36
37
    public const COLLFC_INSERTED = 4; // Collection has been inserted into releases
38
39
    public const COLLFC_DELETE = 5; // Collection is ready for deletion
40
41
    public const COLLFC_TEMPCOMP = 15; // Collection is complete and being checked for complete parts
42
43
    public const COLLFC_ZEROPART = 16; // Collection has a 00/0XX designator (temporary)
44
45
    public const FILE_INCOMPLETE = 0; // We don't have all the parts yet for the file (binaries table partcheck column).
46
47
    public const FILE_COMPLETE = 1; // We have all the parts for the file (binaries table partcheck column).
48
49
    public int $collectionDelayTime;
50
51
    public int $crossPostTime;
52
53
    public int $releaseCreationLimit;
54
55
    public int $completion;
56
57
    public bool $echoCLI;
58
59
    public \PDO $pdo;
60
61
    public ColorCLI $colorCLI;
62
63
    public NZB $nzb;
64
65
    public ReleaseCleaning $releaseCleaning;
66
67
    public Releases $releases;
68
69
    public ReleaseImage $releaseImage;
70
71
    /**
72
     * Time (hours) to wait before delete a stuck/broken collection.
73
     */
74
    private int $collectionTimeout;
75
76
    public function __construct()
77
    {
78
        $this->echoCLI = config('nntmux.echocli');
79
80
        $this->colorCLI = new ColorCLI();
81
        $this->nzb = new NZB();
82
        $this->releaseCleaning = new ReleaseCleaning();
83
        $this->releases = new Releases();
84
        $this->releaseImage = new ReleaseImage();
85
86
        $dummy = Settings::settingValue('..delaytime');
87
        $this->collectionDelayTime = ($dummy !== '' ? (int) $dummy : 2);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
88
        $dummy = Settings::settingValue('..crossposttime');
89
        $this->crossPostTime = ($dummy !== '' ? (int) $dummy : 2);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
90
        $dummy = Settings::settingValue('..maxnzbsprocessed');
91
        $this->releaseCreationLimit = ($dummy !== '' ? (int) $dummy : 1000);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
92
        $dummy = Settings::settingValue('..completionpercent');
93
        $this->completion = ($dummy !== '' ? (int) $dummy : 0);
0 ignored issues
show
introduced by
The condition $dummy !== '' is always true.
Loading history...
94
        if ($this->completion > 100) {
95
            $this->completion = 100;
96
            $this->colorCLI->error(PHP_EOL.'You have an invalid setting for completion. It cannot be higher than 100.');
97
        }
98
        $this->collectionTimeout = (int) Settings::settingValue('indexer.processing.collection_timeout');
99
    }
100
101
    /**
102
     * Main method for creating releases/NZB files from collections.
103
     *
104
     * @param  string  $groupName  (optional)
105
     *
106
     * @throws \Throwable
107
     */
108
    public function processReleases(int $categorize, int $postProcess, string $groupName, NNTP $nntp): int
109
    {
110
        $this->echoCLI = config('nntmux.echocli');
111
        $groupID = '';
112
113
        if (! empty($groupName)) {
114
            $groupInfo = UsenetGroup::getByName($groupName);
115
            if ($groupInfo !== null) {
116
                $groupID = $groupInfo['id'];
117
            }
118
        }
119
120
        if ($this->echoCLI) {
121
            $this->colorCLI->header('Starting release update process ('.now()->format('Y-m-d H:i:s').')');
122
        }
123
124
        if (! file_exists(Settings::settingValue('..nzbpath'))) {
125
            if ($this->echoCLI) {
126
                $this->colorCLI->error('Bad or missing nzb directory - '.Settings::settingValue('..nzbpath'));
127
            }
128
129
            return 0;
130
        }
131
132
        $this->processIncompleteCollections($groupID);
133
        $this->processCollectionSizes($groupID);
134
        $this->deleteUnwantedCollections($groupID);
135
136
        $totalReleasesAdded = 0;
137
        do {
138
            $releasesCount = $this->createReleases($groupID);
139
            $totalReleasesAdded += $releasesCount['added'];
140
141
            $nzbFilesAdded = $this->createNZBs($groupID);
142
143
            $this->categorizeReleases($categorize, $groupID);
144
            $this->postProcessReleases($postProcess, $nntp);
145
            $this->deleteCollections($groupID);
146
147
            // This loops as long as the number of releases or nzbs added was >= the limit (meaning there are more waiting to be created)
148
        } while (
149
            ($releasesCount['added'] + $releasesCount['dupes']) >= $this->releaseCreationLimit
150
            || $nzbFilesAdded >= $this->releaseCreationLimit
151
        );
152
153
        $this->deleteReleases();
154
155
        return $totalReleasesAdded;
156
    }
157
158
    /**
159
     * Return all releases to other->misc category.
160
     *
161
     * @param  string  $where  Optional "where" query parameter.
162
     *
163
     * @void
164
     */
165
    public function resetCategorize(string $where = ''): void
166
    {
167
        DB::update(
168
            sprintf('UPDATE releases SET categories_id = %d, iscategorized = 0 %s', Category::OTHER_MISC, $where)
169
        );
170
    }
171
172
    /**
173
     * Categorizes releases.
174
     *
175
     * @param  string  $type  name or searchname | Categorize using the search name or subject.
176
     * @return int Quantity of categorized releases.
177
     *
178
     * @throws \Exception
179
     */
180
    public function categorizeRelease(string $type, $groupId): int
181
    {
182
        $cat = new Categorize();
183
        $categorized = $total = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $total is dead and can be removed.
Loading history...
184
        $releasesQuery = Release::query()->where(['categories_id' => Category::OTHER_MISC, 'iscategorized' => 0]);
185
        if (! empty($groupId)) {
186
            $releasesQuery->where('groups_id', $groupId);
187
        }
188
        $releases = $releasesQuery->select(['id', 'fromname', 'groups_id', $type])->get();
189
        if ($releases->count() > 0) {
190
            $total = \count($releases);
191
            foreach ($releases as $release) {
192
                $catId = $cat->determineCategory($release->groups_id, $release->{$type}, $release->fromname);
193
                Release::query()->where('id', $release->id)->update(['categories_id' => $catId['categories_id'], 'iscategorized' => 1]);
194
                $categorized++;
195
                if ($this->echoCLI) {
196
                    $this->colorCLI->overWritePrimary(
0 ignored issues
show
introduced by
The method overWritePrimary() does not exist on Blacklight\ColorCLI. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

196
                    $this->colorCLI->/** @scrutinizer ignore-call */ 
197
                                     overWritePrimary(
Loading history...
197
                        'Categorizing: '.$this->colorCLI->percentString($categorized, $total)
0 ignored issues
show
introduced by
The method percentString() does not exist on Blacklight\ColorCLI. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

197
                        'Categorizing: '.$this->colorCLI->/** @scrutinizer ignore-call */ percentString($categorized, $total)
Loading history...
198
                    );
199
                }
200
            }
201
        }
202
        if ($this->echoCLI && $categorized > 0) {
203
            echo PHP_EOL;
204
        }
205
206
        return $categorized;
207
    }
208
209
    /**
210
     * @throws \Exception
211
     * @throws \Throwable
212
     */
213
    public function processIncompleteCollections($groupID): void
214
    {
215
        $startTime = now()->toImmutable();
216
217
        if ($this->echoCLI) {
218
            $this->colorCLI->header('Process Releases -> Attempting to find complete collections.');
219
        }
220
221
        $where = (! empty($groupID) ? ' AND c.groups_id = '.$groupID.' ' : ' ');
222
223
        $this->processStuckCollections($groupID);
224
        $this->collectionFileCheckStage1($groupID);
225
        $this->collectionFileCheckStage2($groupID);
226
        $this->collectionFileCheckStage3($where);
227
        $this->collectionFileCheckStage4($where);
228
        $this->collectionFileCheckStage5($groupID);
229
        $this->collectionFileCheckStage6($where);
230
231
        if ($this->echoCLI) {
232
            $countQuery = Collection::query()->where('filecheck', self::COLLFC_COMPPART);
233
234
            if (! empty($groupID)) {
235
                $countQuery->where('groups_id', $groupID);
236
            }
237
            $count = $countQuery->count('id');
238
239
            $totalTime = now()->diffInSeconds($startTime);
240
241
            $this->colorCLI->primary(
242
                ($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

242
                (/** @scrutinizer ignore-type */ $count ?? 0).' collections were found to be complete. Time: '.
Loading history...
243
                $totalTime.Str::plural(' second', $totalTime),
244
                true
245
            );
246
        }
247
    }
248
249
    /**
250
     * @throws \Exception
251
     * @throws \Throwable
252
     */
253
    public function processCollectionSizes($groupID): void
254
    {
255
        $startTime = now()->toImmutable();
256
257
        if ($this->echoCLI) {
258
            $this->colorCLI->header('Process Releases -> Calculating collection sizes (in bytes).');
259
        }
260
        // Get the total size in bytes of the collection for collections where filecheck = 2.
261
        DB::transaction(function () use ($groupID, $startTime) {
262
            $checked = DB::update(
263
                sprintf(
264
                    '
265
				UPDATE collections c
266
				SET c.filesize =
267
				(
268
					SELECT COALESCE(SUM(b.partsize), 0)
269
					FROM binaries b
270
					WHERE b.collections_id = c.id
271
				),
272
				c.filecheck = %d
273
				WHERE c.filecheck = %d
274
				AND c.filesize = 0 %s',
275
                    self::COLLFC_SIZED,
276
                    self::COLLFC_COMPPART,
277
                    (! empty($groupID) ? ' AND c.groups_id = '.$groupID : ' ')
278
                )
279
            );
280
            if ($checked > 0 && $this->echoCLI) {
281
                $this->colorCLI->primary(
282
                    $checked.' collections set to filecheck = 3(size calculated)',
283
                    true
284
                );
285
                $totalTime = now()->diffInSeconds($startTime);
286
                $this->colorCLI->primary($totalTime.Str::plural(' second', $totalTime), true);
287
            }
288
        }, 10);
289
    }
290
291
    /**
292
     * @throws \Exception
293
     * @throws \Throwable
294
     */
295
    public function deleteUnwantedCollections($groupID): void
296
    {
297
        $startTime = now()->toImmutable();
298
299
        if ($this->echoCLI) {
300
            $this->colorCLI->header('Process Releases -> Delete collections smaller/larger than minimum size/file count from group/site setting.');
301
        }
302
303
        $groupID === '' ? $groupIDs = UsenetGroup::getActiveIDs() : $groupIDs = [['id' => $groupID]];
304
305
        $minSizeDeleted = $maxSizeDeleted = $minFilesDeleted = 0;
306
307
        $maxSizeSetting = Settings::settingValue('.release.maxsizetoformrelease');
308
        $minSizeSetting = Settings::settingValue('.release.minsizetoformrelease');
309
        $minFilesSetting = Settings::settingValue('.release.minfilestoformrelease');
310
311
        foreach ($groupIDs as $grpID) {
312
            $groupMinSizeSetting = $groupMinFilesSetting = 0;
313
314
            $groupMinimums = UsenetGroup::getGroupByID($grpID['id']);
315
            if ($groupMinimums !== null) {
316
                if (! empty($groupMinimums['minsizetoformrelease']) && $groupMinimums['minsizetoformrelease'] > 0) {
317
                    $groupMinSizeSetting = (int) $groupMinimums['minsizetoformrelease'];
318
                }
319
                if (! empty($groupMinimums['minfilestoformrelease']) && $groupMinimums['minfilestoformrelease'] > 0) {
320
                    $groupMinFilesSetting = (int) $groupMinimums['minfilestoformrelease'];
321
                }
322
            }
323
324
            if (Collection::query()->where('filecheck', self::COLLFC_SIZED)->where('filesize', '>', 0)->first() !== null) {
325
                DB::transaction(function () use (
326
                    $groupMinSizeSetting,
327
                    $minSizeSetting,
328
                    $minSizeDeleted,
329
                    $maxSizeSetting,
330
                    $maxSizeDeleted,
331
                    $minFilesSetting,
332
                    $groupMinFilesSetting,
333
                    $minFilesDeleted,
334
                    $startTime
335
                ) {
336
                    $deleteQuery = Collection::query()
337
                        ->where('filecheck', self::COLLFC_SIZED)
338
                        ->where('filesize', '>', 0)
339
                        ->whereRaw('GREATEST(?, ?) > 0 AND filesize < GREATEST(?, ?)', [$groupMinSizeSetting, $minSizeSetting, $groupMinSizeSetting, $minSizeSetting])
340
                        ->delete();
341
342
                    if ($deleteQuery > 0) {
343
                        $minSizeDeleted += $deleteQuery;
344
                    }
345
346
                    if ($maxSizeSetting > 0) {
347
                        $deleteQuery = Collection::query()
348
                            ->where('filecheck', '=', self::COLLFC_SIZED)
349
                            ->where('filesize', '>', $maxSizeSetting)
350
                            ->delete();
351
352
                        if ($deleteQuery > 0) {
353
                            $maxSizeDeleted += $deleteQuery;
354
                        }
355
                    }
356
357
                    if ($minFilesSetting > 0 || $groupMinFilesSetting > 0) {
358
                        $deleteQuery = Collection::query()
359
                            ->where('filecheck', self::COLLFC_SIZED)
360
                            ->where('filesize', '>', 0)
361
                            ->whereRaw('GREATEST(?, ?) > 0 AND totalfiles < GREATEST(?, ?)', [$groupMinFilesSetting, $minFilesSetting, $groupMinFilesSetting, $minFilesSetting])
362
                            ->delete();
363
364
                        if ($deleteQuery > 0) {
365
                            $minFilesDeleted += $deleteQuery;
366
                        }
367
                    }
368
369
                    $totalTime = now()->diffInSeconds($startTime);
370
371
                    if ($this->echoCLI) {
372
                        $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);
373
                    }
374
                }, 10);
375
            }
376
        }
377
    }
378
379
    /**
380
     * @param  int|string  $groupID  (optional)
381
     *
382
     * @throws \Throwable
383
     */
384
    #[ArrayShape(['added' => 'int', 'dupes' => 'int'])]
385
    public function createReleases(int|string $groupID): array
386
    {
387
        $startTime = now()->toImmutable();
388
389
        $categorize = new Categorize();
390
        $returnCount = $duplicate = 0;
391
392
        if ($this->echoCLI) {
393
            $this->colorCLI->header('Process Releases -> Create releases from complete collections.');
394
        }
395
        $collectionsQuery = Collection::query()
396
            ->where('collections.filecheck', self::COLLFC_SIZED)
397
            ->where('collections.filesize', '>', 0);
398
        if (! empty($groupID)) {
399
            $collectionsQuery->where('collections.groups_id', $groupID);
400
        }
401
        $collectionsQuery->select(['collections.*', 'usenet_groups.name as gname'])
402
            ->join('usenet_groups', 'usenet_groups.id', '=', 'collections.groups_id')
403
            ->limit($this->releaseCreationLimit);
404
        $collections = $collectionsQuery->get();
405
        if ($this->echoCLI && $collections->count() > 0) {
406
            $this->colorCLI->primary(\count($collections).' Collections ready to be converted to releases.', true);
407
        }
408
409
        foreach ($collections as $collection) {
410
            $cleanRelName = mb_convert_encoding(str_replace(['#', '@', '$', '%', '^', '§', '¨', '©', 'Ö'], '', $collection->subject), 'UTF-8', mb_list_encodings());
411
            $fromName = mb_convert_encoding(
412
                trim($collection->fromname, "'"), 'UTF-8', mb_list_encodings()
413
            );
414
415
            // Look for duplicates, duplicates match on releases.name, releases.fromname and releases.size
416
            // A 1% variance in size is considered the same size when the subject and poster are the same
417
            $dupeCheck = Release::query()
418
                ->where(['name' => $cleanRelName, 'fromname' => $fromName])
419
                ->whereBetween('size', [$collection->filesize * .99, $collection->filesize * 1.01])
420
                ->first(['id']);
421
422
            if ($dupeCheck === null) {
423
                $cleanedName = $this->releaseCleaning->releaseCleaner(
424
                    $collection->subject,
425
                    $collection->fromname,
426
                    $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...
427
                );
428
429
                if (\is_array($cleanedName)) {
430
                    $properName = $cleanedName['properlynamed'] ?? false;
431
                    $preID = $cleanedName['predb'] ?? false;
432
                    $cleanedName = $cleanedName['cleansubject'] ?? $cleanRelName;
433
                } else {
434
                    $properName = true;
435
                    $preID = false;
436
                }
437
438
                if ($preID === false && $cleanedName !== '') {
439
                    // try to match the cleaned searchname to predb title or filename here
440
                    $preMatch = Predb::matchPre($cleanedName);
441
                    if ($preMatch !== false) {
442
                        $cleanedName = $preMatch['title'];
443
                        $preID = $preMatch['predb_id'];
444
                        $properName = true;
445
                    }
446
                }
447
448
                $determinedCategory = $categorize->determineCategory($collection->groups_id, $cleanedName);
449
450
                $releaseID = Release::insertRelease(
451
                    [
452
                        'name' => $cleanRelName,
453
                        'searchname' => ! empty($cleanedName) ? mb_convert_encoding($cleanedName, 'UTF-8', mb_list_encodings()) : $cleanRelName,
454
                        'totalpart' => $collection->totalfiles,
455
                        'groups_id' => $collection->groups_id,
456
                        'guid' => createGUID(),
457
                        'postdate' => $collection->date,
458
                        'fromname' => $fromName,
459
                        'size' => $collection->filesize,
460
                        'categories_id' => $determinedCategory['categories_id'] ?? Category::OTHER_MISC,
461
                        'isrenamed' => $properName === true ? 1 : 0,
462
                        'predb_id' => $preID === false ? 0 : $preID,
463
                        'nzbstatus' => NZB::NZB_NONE,
464
                    ]
465
                );
466
467
                if ($releaseID !== null) {
468
                    // Update collections table to say we inserted the release.
469
                    DB::transaction(static function () use ($collection, $releaseID) {
470
                        Collection::query()->where('id', $collection->id)->update(['filecheck' => self::COLLFC_INSERTED, 'releases_id' => $releaseID]);
471
                    }, 10);
472
473
                    // Add the id of regex that matched the collection and release name to release_regexes table
474
                    ReleaseRegex::insertOrIgnore([
475
                        'releases_id' => $releaseID,
476
                        'collection_regex_id' => $collection->collection_regexes_id,
477
                        'naming_regex_id' => $cleanedName['id'] ?? 0,
478
                    ]);
479
480
                    if (preg_match_all('#(\S+):\S+#', $collection->xref, $hits)) {
481
                        foreach ($hits[1] as $grp) {
482
                            //check if the group name is in a valid format
483
                            $grpTmp = UsenetGroup::isValidGroup($grp);
484
                            if ($grpTmp !== false) {
485
                                //check if the group already exists in database
486
                                $xrefGrpID = UsenetGroup::getIDByName($grpTmp);
487
                                if ($xrefGrpID === '') {
488
                                    $xrefGrpID = UsenetGroup::addGroup(
489
                                        [
490
                                            'name' => $grpTmp,
491
                                            'description' => 'Added by Release processing',
492
                                            'backfill_target' => 1,
493
                                            'first_record' => 0,
494
                                            'last_record' => 0,
495
                                            'active' => 0,
496
                                            'backfill' => 0,
497
                                            'minfilestoformrelease' => '',
498
                                            'minsizetoformrelease' => '',
499
                                        ]
500
                                    );
501
                                }
502
503
                                $relGroupsChk = ReleasesGroups::query()->where(
504
                                    [
505
                                        ['releases_id', '=', $releaseID],
506
                                        ['groups_id', '=', $xrefGrpID],
507
                                    ]
508
                                )->first();
509
510
                                if ($relGroupsChk === null) {
511
                                    ReleasesGroups::query()->insert(
512
                                        [
513
                                            'releases_id' => $releaseID,
514
                                            'groups_id' => $xrefGrpID,
515
                                        ]
516
                                    );
517
                                }
518
                            }
519
                        }
520
                    }
521
522
                    $returnCount++;
523
524
                    if ($this->echoCLI) {
525
                        echo "Added $returnCount releases.\r";
526
                    }
527
                }
528
            } else {
529
                // The release was already in the DB, so delete the collection.
530
                DB::transaction(static function () use ($collection) {
531
                    Collection::query()->where('collectionhash', $collection->collectionhash)->delete();
532
                }, 10);
533
534
                $duplicate++;
535
            }
536
        }
537
538
        $totalTime = now()->diffInSeconds($startTime);
539
540
        if ($this->echoCLI) {
541
            $this->colorCLI->primary(
542
                PHP_EOL.
543
                number_format($returnCount).
544
                ' Releases added and '.
545
                number_format($duplicate).
546
                ' duplicate collections deleted in '.
547
                $totalTime.Str::plural(' second', $totalTime),
548
                true
549
            );
550
        }
551
552
        return ['added' => $returnCount, 'dupes' => $duplicate];
553
    }
554
555
    /**
556
     * Create NZB files from complete releases.
557
     *
558
     * @param  int|string  $groupID  (optional)
559
     *
560
     * @throws \Throwable
561
     */
562
    public function createNZBs(int|string $groupID): int
563
    {
564
        $startTime = now()->toImmutable();
565
566
        if ($this->echoCLI) {
567
            $this->colorCLI->header('Process Releases -> Create the NZB, delete collections/binaries/parts.');
568
        }
569
570
        $releasesQuery = Release::query()->with('category.parent')->where('nzbstatus', '=', 0);
571
        if (! empty($groupID)) {
572
            $releasesQuery->where('releases.groups_id', $groupID);
573
        }
574
        $releases = $releasesQuery->select(['id', 'guid', 'name', 'categories_id'])->get();
575
576
        $nzbCount = 0;
577
578
        if ($releases->count() > 0) {
579
            $total = $releases->count();
580
            foreach ($releases as $release) {
581
                if ($this->nzb->writeNzbForReleaseId($release)) {
582
                    $nzbCount++;
583
                    if ($this->echoCLI) {
584
                        echo "Creating NZBs and deleting Collections: $nzbCount/$total.\r";
585
                    }
586
                }
587
            }
588
        }
589
590
        $totalTime = now()->diffInSeconds($startTime);
591
592
        if ($this->echoCLI) {
593
            $this->colorCLI->primary(
594
                number_format($nzbCount).' NZBs created/Collections deleted in '.
595
                $totalTime.Str::plural(' second', $totalTime).PHP_EOL.
596
                'Total time: '.$totalTime.Str::plural(' second', $totalTime),
597
                true
598
            );
599
        }
600
601
        return $nzbCount;
602
    }
603
604
    /**
605
     * Categorize releases.
606
     *
607
     * @param  int|string  $groupID  (optional)
608
     *
609
     * @void
610
     *
611
     * @throws \Exception
612
     */
613
    public function categorizeReleases(int $categorize, int|string $groupID = ''): void
614
    {
615
        $startTime = now()->toImmutable();
616
        if ($this->echoCLI) {
617
            $this->colorCLI->header('Process Releases -> Categorize releases.');
618
        }
619
        $type = match ((int) $categorize) {
620
            2 => 'searchname',
621
            default => 'name',
622
        };
623
        $this->categorizeRelease(
624
            $type,
625
            $groupID
626
        );
627
628
        $totalTime = now()->diffInSeconds($startTime);
629
630
        if ($this->echoCLI) {
631
            $this->colorCLI->primary($totalTime.Str::plural(' second', $totalTime));
632
        }
633
    }
634
635
    /**
636
     * Post-process releases.
637
     *
638
     *
639
     * @void
640
     *
641
     * @throws \Exception
642
     */
643
    public function postProcessReleases(int $postProcess, NNTP $nntp): void
644
    {
645
        if ((int) $postProcess === 1) {
646
            (new PostProcess(['Echo' => $this->echoCLI]))->processAll($nntp);
0 ignored issues
show
Unused Code introduced by
The call to Blacklight\processing\PostProcess::__construct() has too many arguments starting with array('Echo' => $this->echoCLI). ( Ignorable by Annotation )

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

646
            (/** @scrutinizer ignore-call */ new PostProcess(['Echo' => $this->echoCLI]))->processAll($nntp);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
647
        } elseif ($this->echoCLI) {
648
            $this->colorCLI->info(
649
                'Post-processing is not running inside the Process Releases class.'.PHP_EOL.
650
                'If you are using tmux or screen they might have their own scripts running Post-processing.'
651
            );
652
        }
653
    }
654
655
    /**
656
     * @throws \Exception
657
     * @throws \Throwable
658
     */
659
    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

659
    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...
660
    {
661
        $startTime = now()->toImmutable();
662
663
        $deletedCount = 0;
664
665
        // CBP older than retention.
666
        if ($this->echoCLI) {
667
            echo $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

667
            echo /** @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...
668
                $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...
669
                    'Deleting collections/binaries/parts older than %d hours.',
670
                    Settings::settingValue('..partretentionhours')
671
                ), true);
672
        }
673
674
        DB::transaction(function () use ($deletedCount, $startTime) {
675
            $deleted = 0;
676
            $deleteQuery = Collection::query()
677
                ->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

677
                ->where('dateadded', '<', now()->subHours(/** @scrutinizer ignore-type */ Settings::settingValue('..partretentionhours')))
Loading history...
678
                ->delete();
679
            if ($deleteQuery > 0) {
680
                $deleted = $deleteQuery;
681
                $deletedCount += $deleted;
682
            }
683
            $firstQuery = $fourthQuery = now();
684
685
            $totalTime = $firstQuery->diffInSeconds($startTime);
686
687
            if ($this->echoCLI) {
688
                $this->colorCLI->primary(
689
                    'Finished deleting '.$deleted.' old collections/binaries/parts in '.
690
                    $totalTime.Str::plural(' second', $totalTime),
691
                    true
692
                );
693
            }
694
695
            // Cleanup orphaned collections, binaries and parts
696
            // this really shouldn't happen, but just incase - so we only run 1/200 of the time
697
            if (random_int(0, 200) <= 1) {
698
                // CBP collection orphaned with no binaries or parts.
699
                if ($this->echoCLI) {
700
                    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->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

700
                    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...
701
                }
702
703
                $deleted = 0;
704
                $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();
705
706
                if ($deleteQuery > 0) {
707
                    $deleted = $deleteQuery;
708
                    $deletedCount += $deleted;
709
                }
710
711
                $totalTime = now()->diffInSeconds($firstQuery);
712
713
                if ($this->echoCLI) {
714
                    $this->colorCLI->primary('Finished deleting '.$deleted.' orphaned collections in '.$totalTime.Str::plural(' second', $totalTime), true);
715
                }
716
            }
717
718
            if ($this->echoCLI) {
719
                $this->colorCLI->primary('Deleting collections that were missed after NZB creation.', true);
720
            }
721
722
            $deleted = 0;
723
            // Collections that were missing on NZB creation.
724
            $collections = Collection::query()->where('releases.nzbstatus', '=', 1)->leftJoin('releases', 'releases.id', '=', 'collections.releases_id')->select('collections.id')->get();
725
726
            foreach ($collections as $collection) {
727
                $deleted++;
728
                Collection::query()->where('id', $collection->id)->delete();
729
            }
730
            $deletedCount += $deleted;
731
732
            $colDelTime = now()->diffInSeconds($fourthQuery);
733
            $totalTime = $fourthQuery->diffInSeconds($startTime);
734
735
            if ($this->echoCLI) {
736
                $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);
737
            }
738
        }, 10);
739
    }
740
741
    /**
742
     * Delete unwanted releases based on admin settings.
743
     * This deletes releases based on group.
744
     *
745
     * @param  int|string  $groupID  (optional)
746
     *
747
     * @void
748
     *
749
     * @throws \Exception
750
     */
751
    public function deletedReleasesByGroup(int|string $groupID = ''): void
752
    {
753
        $startTime = now()->toImmutable();
754
        $minSizeDeleted = $maxSizeDeleted = $minFilesDeleted = 0;
755
756
        if ($this->echoCLI) {
757
            $this->colorCLI->header('Process Releases -> Delete releases smaller/larger than minimum size/file count from group/site setting.');
758
        }
759
760
        $groupIDs = $groupID === '' ? UsenetGroup::getActiveIDs() : [['id' => $groupID]];
761
762
        $maxSizeSetting = Settings::settingValue('.release.maxsizetoformrelease');
763
        $minSizeSetting = Settings::settingValue('.release.minsizetoformrelease');
764
        $minFilesSetting = Settings::settingValue('.release.minfilestoformrelease');
765
766
        foreach ($groupIDs as $grpID) {
767
            $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();
768
            foreach ($releases as $release) {
769
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
770
                $minSizeDeleted++;
771
            }
772
773
            if ($maxSizeSetting > 0) {
774
                $releases = Release::query()->where('groups_id', $grpID['id'])->where('size', '>', $maxSizeSetting)->select(['id', 'guid'])->get();
775
                foreach ($releases as $release) {
776
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
777
                    $maxSizeDeleted++;
778
                }
779
            }
780
            if ($minFilesSetting > 0) {
781
                $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();
782
                foreach ($releases as $release) {
783
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
784
                    $minFilesDeleted++;
785
                }
786
            }
787
        }
788
789
        $totalTime = now()->diffInSeconds($startTime);
790
791
        if ($this->echoCLI) {
792
            $this->colorCLI->primary(
793
                'Deleted '.($minSizeDeleted + $maxSizeDeleted + $minFilesDeleted).
794
                ' releases: '.PHP_EOL.
795
                $minSizeDeleted.' smaller than, '.$maxSizeDeleted.' bigger than, '.$minFilesDeleted.
796
                ' with less files than site/groups setting in: '.
797
                $totalTime.Str::plural(' second', $totalTime),
798
                true
799
            );
800
        }
801
    }
802
803
    /**
804
     * Delete releases using admin settings.
805
     * This deletes releases, regardless of group.
806
     *
807
     * @void
808
     *
809
     * @throws \Exception
810
     */
811
    public function deleteReleases(): void
812
    {
813
        $startTime = now()->toImmutable();
814
        $genres = new Genres();
815
        $passwordDeleted = $duplicateDeleted = $retentionDeleted = $completionDeleted = $disabledCategoryDeleted = 0;
816
        $disabledGenreDeleted = $miscRetentionDeleted = $miscHashedDeleted = $categoryMinSizeDeleted = 0;
817
818
        // Delete old releases and finished collections.
819
        if ($this->echoCLI) {
820
            $this->colorCLI->header('Process Releases -> Delete old releases and passworded releases.');
821
        }
822
823
        // Releases past retention.
824
        if ((int) Settings::settingValue('..releaseretentiondays') !== 0) {
825
            $releases = Release::query()->where('postdate', '<', now()->subDays((int) Settings::settingValue('..releaseretentiondays')))->select(['id', 'guid'])->get();
826
            foreach ($releases as $release) {
827
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
828
                $retentionDeleted++;
829
            }
830
        }
831
832
        // Passworded releases.
833
        if ((int) Settings::settingValue('..deletepasswordedrelease') === 1) {
834
            $releases = Release::query()->join('release_files', 'release_files.releases_id', '=', 'releases.id')->select(['id', 'guid'])->where('release_files.passworded', '=', Releases::PASSWD_RAR)->orWhere('passwordstatus', '=', Releases::PASSWD_RAR)->get();
835
            foreach ($releases as $release) {
836
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
837
                $passwordDeleted++;
838
            }
839
        }
840
841
        if ((int) $this->crossPostTime !== 0) {
842
            // Cross posted releases.
843
            $releases = Release::query()->where('adddate', '>', now()->subHours($this->crossPostTime))->havingRaw('COUNT(name) > 1')->groupBy('name')->select(['id', 'guid'])->get();
844
            foreach ($releases as $release) {
845
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
846
                $duplicateDeleted++;
847
            }
848
        }
849
850
        if ($this->completion > 0) {
851
            $releases = Release::query()->where('completion', '<', $this->completion)->where('completion', '>', 0)->select(['id', 'guid'])->get();
852
            foreach ($releases as $release) {
853
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
854
                $completionDeleted++;
855
            }
856
        }
857
858
        // Disabled categories.
859
        $disabledCategories = Category::getDisabledIDs();
860
        if (\count($disabledCategories) > 0) {
861
            foreach ($disabledCategories as $disabledCategory) {
862
                $releases = Release::query()->where('categories_id', (int) $disabledCategory['id'])->select(['id', 'guid'])->get();
863
                foreach ($releases as $release) {
864
                    $disabledCategoryDeleted++;
865
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
866
                }
867
            }
868
        }
869
870
        // Delete smaller than category minimum sizes.
871
        $categories = Category::query()->select(['id', 'minsizetoformrelease as minsize'])->get();
872
873
        foreach ($categories as $category) {
874
            if ((int) $category->minsize > 0) {
875
                $releases = Release::query()->where('categories_id', (int) $category->id)->where('size', '<', (int) $category->minsize)->select(['id', 'guid'])->limit(1000)->get();
876
                foreach ($releases as $release) {
877
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
878
                    $categoryMinSizeDeleted++;
879
                }
880
            }
881
        }
882
883
        // Disabled music genres.
884
        $genrelist = $genres->getDisabledIDs();
885
        if (\count($genrelist) > 0) {
886
            foreach ($genrelist as $genre) {
887
                $musicInfoQuery = MusicInfo::query()->where('genre_id', (int) $genre['id'])->select(['id']);
888
                $releases = Release::query()
889
                    ->joinSub($musicInfoQuery, 'mi', function ($join) {
890
                        $join->on('releases.musicinfo_id', '=', 'mi.id');
891
                    })
892
                    ->select(['releases.id', 'releases.guid'])
893
                    ->get();
894
                foreach ($releases as $release) {
895
                    $disabledGenreDeleted++;
896
                    $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
897
                }
898
            }
899
        }
900
901
        // Misc other.
902
        if (Settings::settingValue('..miscotherretentionhours') > 0) {
903
            $releases = Release::query()->where('categories_id', Category::OTHER_MISC)->where('adddate', '<=', now()->subHours((int) Settings::settingValue('..miscotherretentionhours')))->select(['id', 'guid'])->get();
904
            foreach ($releases as $release) {
905
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
906
                $miscRetentionDeleted++;
907
            }
908
        }
909
910
        // Misc hashed.
911
        if ((int) Settings::settingValue('..mischashedretentionhours') > 0) {
912
            $releases = Release::query()->where('categories_id', Category::OTHER_HASHED)->where('adddate', '<=', now()->subHours((int) Settings::settingValue('..mischashedretentionhours')))->select(['id', 'guid'])->get();
913
            foreach ($releases as $release) {
914
                $this->releases->deleteSingle(['g' => $release->guid, 'i' => $release->id], $this->nzb, $this->releaseImage);
915
                $miscHashedDeleted++;
916
            }
917
        }
918
919
        if ($this->echoCLI) {
920
            $this->colorCLI->primary(
921
                'Removed releases: '.
922
                number_format($retentionDeleted).
923
                ' past retention, '.
924
                number_format($passwordDeleted).
925
                ' passworded, '.
926
                number_format($duplicateDeleted).
927
                ' crossposted, '.
928
                number_format($disabledCategoryDeleted).
929
                ' from disabled categories, '.
930
                number_format($categoryMinSizeDeleted).
931
                ' smaller than category settings, '.
932
                number_format($disabledGenreDeleted).
933
                ' from disabled music genres, '.
934
                number_format($miscRetentionDeleted).
935
                ' from misc->other '.
936
                number_format($miscHashedDeleted).
937
                ' from misc->hashed'.
938
                (
939
                    $this->completion > 0
940
                    ? ', '.number_format($completionDeleted).' under '.$this->completion.'% completion.'
941
                    : '.'
942
                ),
943
                true
944
            );
945
946
            $totalDeleted = (
947
                $retentionDeleted + $passwordDeleted + $duplicateDeleted + $disabledCategoryDeleted +
948
                $disabledGenreDeleted + $miscRetentionDeleted + $miscHashedDeleted + $completionDeleted +
949
                $categoryMinSizeDeleted
950
            );
951
            if ($totalDeleted > 0) {
952
                $totalTime = now()->diffInSeconds($startTime);
953
                $this->colorCLI->primary(
954
                    'Removed '.number_format($totalDeleted).' releases in '.
955
                    $totalTime.Str::plural(' second', $totalTime),
956
                    true
957
                );
958
            }
959
        }
960
    }
961
962
    /**
963
     * Look if we have all the files in a collection (which have the file count in the subject).
964
     * Set file check to complete.
965
     * This means the the binary table has the same count as the file count in the subject, but
966
     * the collection might not be complete yet since we might not have all the articles in the parts table.
967
     *
968
     *
969
     * @void
970
     *
971
     * @throws \Throwable
972
     */
973
    private function collectionFileCheckStage1(int $groupID): void
974
    {
975
        DB::transaction(static function () use ($groupID) {
976
            $collectionsCheck = Collection::query()->select(['collections.id'])
977
                ->join('binaries', 'binaries.collections_id', '=', 'collections.id')
978
                ->where('collections.totalfiles', '>', 0)
979
                ->where('collections.filecheck', '=', self::COLLFC_DEFAULT);
980
            if (! empty($groupID)) {
981
                $collectionsCheck->where('collections.groups_id', $groupID);
982
            }
983
            $collectionsCheck->groupBy('binaries.collections_id', 'collections.totalfiles', 'collections.id')
984
                ->havingRaw('COUNT(binaries.id) IN (collections.totalfiles, collections.totalfiles+1)');
985
986
            Collection::query()->joinSub($collectionsCheck, 'r', function ($join) {
987
                $join->on('collections.id', '=', 'r.id');
988
            })->update(['collections.filecheck' => self::COLLFC_COMPCOLL]);
989
        }, 10);
990
    }
991
992
    /**
993
     * The first query sets filecheck to COLLFC_ZEROPART if there's a file that starts with 0 (ex. [00/100]).
994
     * The second query sets filecheck to COLLFC_TEMPCOMP on everything left over, so anything that starts with 1 (ex. [01/100]).
995
     *
996
     * This is done because some collections start at 0 and some at 1, so if you were to assume the collection is complete
997
     * at 0 then you would never get a complete collection if it starts with 1 and if it starts, you can end up creating
998
     * a incomplete collection, since you assumed it was complete.
999
     *
1000
     *
1001
     * @void
1002
     *
1003
     * @throws \Throwable
1004
     */
1005
    private function collectionFileCheckStage2(int $groupID): void
1006
    {
1007
        DB::transaction(static function () use ($groupID) {
1008
            $collectionsCheck = Collection::query()->select(['collections.id'])
1009
                ->join('binaries', 'binaries.collections_id', '=', 'collections.id')
1010
                ->where('binaries.filenumber', '=', 0)
1011
                ->where('collections.totalfiles', '>', 0)
1012
                ->where('collections.filecheck', '=', self::COLLFC_COMPCOLL);
1013
            if (! empty($groupID)) {
1014
                $collectionsCheck->where('collections.groups_id', $groupID);
1015
            }
1016
            $collectionsCheck->groupBy('collections.id');
1017
1018
            Collection::query()->joinSub($collectionsCheck, 'r', function ($join) {
1019
                $join->on('collections.id', '=', 'r.id');
1020
            })->update(['collections.filecheck' => self::COLLFC_ZEROPART]);
1021
        }, 10);
1022
1023
        DB::transaction(static function () use ($groupID) {
1024
            $collectionQuery = Collection::query()->where('filecheck', '=', self::COLLFC_COMPCOLL);
1025
            if (! empty($groupID)) {
1026
                $collectionQuery->where('groups_id', $groupID);
1027
            }
1028
            $collectionQuery->update(['filecheck' => self::COLLFC_TEMPCOMP]);
1029
        }, 10);
1030
    }
1031
1032
    /**
1033
     * Check if the files (binaries table) in a complete collection has all the parts.
1034
     * If we have all the parts, set binaries table partcheck to FILE_COMPLETE.
1035
     *
1036
     *
1037
     * @void
1038
     *
1039
     * @throws \Throwable
1040
     */
1041
    private function collectionFileCheckStage3(string $where): void
1042
    {
1043
        DB::transaction(static function () use ($where) {
1044
            DB::update(
1045
                sprintf(
1046
                    '
1047
				UPDATE binaries b
1048
				INNER JOIN
1049
				(
1050
					SELECT b.id
1051
					FROM binaries b
1052
					INNER JOIN collections c ON c.id = b.collections_id
1053
					WHERE c.filecheck = %d
1054
					AND b.partcheck = %d %s
1055
					AND b.currentparts = b.totalparts
1056
					GROUP BY b.id, b.totalparts
1057
				) r ON b.id = r.id
1058
				SET b.partcheck = %d',
1059
                    self::COLLFC_TEMPCOMP,
1060
                    self::FILE_INCOMPLETE,
1061
                    $where,
1062
                    self::FILE_COMPLETE
1063
                )
1064
            );
1065
        }, 10);
1066
1067
        DB::transaction(static function () use ($where) {
1068
            DB::update(
1069
                sprintf(
1070
                    '
1071
				UPDATE binaries b
1072
				INNER JOIN
1073
				(
1074
					SELECT b.id
1075
					FROM binaries b
1076
					INNER JOIN collections c ON c.id = b.collections_id
1077
					WHERE c.filecheck = %d
1078
					AND b.partcheck = %d %s
1079
					AND b.currentparts >= (b.totalparts + 1)
1080
					GROUP BY b.id, b.totalparts
1081
				) r ON b.id = r.id
1082
				SET b.partcheck = %d',
1083
                    self::COLLFC_ZEROPART,
1084
                    self::FILE_INCOMPLETE,
1085
                    $where,
1086
                    self::FILE_COMPLETE
1087
                )
1088
            );
1089
        }, 10);
1090
    }
1091
1092
    /**
1093
     * Check if all files (binaries table) for a collection are complete (if they all have the "parts").
1094
     * Set collections filecheck column to COLLFC_COMPPART.
1095
     * This means the collection is complete.
1096
     *
1097
     *
1098
     * @void
1099
     *
1100
     * @throws \Throwable
1101
     */
1102
    private function collectionFileCheckStage4(string &$where): void
1103
    {
1104
        DB::transaction(static function () use ($where) {
1105
            DB::update(
1106
                sprintf(
1107
                    '
1108
				UPDATE collections c INNER JOIN
1109
					(SELECT c.id FROM collections c
1110
					INNER JOIN binaries b ON c.id = b.collections_id
1111
					WHERE b.partcheck = 1 AND c.filecheck IN (%d, %d) %s
1112
					GROUP BY b.collections_id, c.totalfiles, c.id HAVING COUNT(b.id) >= c.totalfiles)
1113
				r ON c.id = r.id SET filecheck = %d',
1114
                    self::COLLFC_TEMPCOMP,
1115
                    self::COLLFC_ZEROPART,
1116
                    $where,
1117
                    self::COLLFC_COMPPART
1118
                )
1119
            );
1120
        }, 10);
1121
    }
1122
1123
    /**
1124
     * If not all files (binaries table) had their parts on the previous stage,
1125
     * reset the collection filecheck column to COLLFC_COMPCOLL so we reprocess them next time.
1126
     *
1127
     *
1128
     * @void
1129
     *
1130
     * @throws \Throwable
1131
     */
1132
    private function collectionFileCheckStage5(int $groupId): void
1133
    {
1134
        DB::transaction(static function () use ($groupId) {
1135
            $collectionQuery = Collection::query()->whereIn('filecheck', [self::COLLFC_TEMPCOMP, self::COLLFC_ZEROPART]);
1136
            if (! empty($groupId)) {
1137
                $collectionQuery->where('groups_id', $groupId);
1138
            }
1139
            $collectionQuery->update(['filecheck' => self::COLLFC_COMPCOLL]);
1140
        }, 10);
1141
    }
1142
1143
    /**
1144
     * If a collection did not have the file count (ie: [00/12]) or the collection is incomplete after
1145
     * $this->collectionDelayTime hours, set the collection to complete to create it into a release/nzb.
1146
     *
1147
     *
1148
     * @void
1149
     *
1150
     * @throws \Throwable
1151
     */
1152
    private function collectionFileCheckStage6(string &$where): void
1153
    {
1154
        DB::transaction(function () use ($where) {
1155
            DB::update(
1156
                sprintf(
1157
                    "
1158
				UPDATE collections c SET filecheck = %d, totalfiles = (SELECT COUNT(b.id) FROM binaries b WHERE b.collections_id = c.id)
1159
				WHERE c.dateadded < NOW() - INTERVAL '%d' HOUR
1160
				AND c.filecheck IN (%d, %d, 10) %s",
1161
                    self::COLLFC_COMPPART,
1162
                    $this->collectionDelayTime,
1163
                    self::COLLFC_DEFAULT,
1164
                    self::COLLFC_COMPCOLL,
1165
                    $where
1166
                )
1167
            );
1168
        }, 10);
1169
    }
1170
1171
    /**
1172
     * If a collection has been stuck for $this->collectionTimeout hours, delete it, it's bad.
1173
     *
1174
     *
1175
     * @void
1176
     *
1177
     * @throws \Exception
1178
     * @throws \Throwable
1179
     */
1180
    private function processStuckCollections(int $groupID): void
1181
    {
1182
        $lastRun = Settings::settingValue('indexer.processing.last_run_time');
1183
1184
        DB::transaction(function () use ($groupID, $lastRun) {
1185
            $objQuery = Collection::query()
1186
                ->where('added', '<', Carbon::createFromFormat('Y-m-d H:i:s', $lastRun)->subHours($this->collectionTimeout));
1187
            if (! empty($groupID)) {
1188
                $objQuery->where('groups_id', $groupID);
1189
            }
1190
            $obj = $objQuery->delete();
1191
            if ($this->echoCLI && $obj > 0) {
1192
                $this->colorCLI->primary('Deleted '.$obj.' broken/stuck collections.', true);
1193
            }
1194
        }, 10);
1195
    }
1196
}
1197