Passed
Push — master ( 2343b7...2a99ce )
by Darko
03:16
created

Releases::apiSearch()   F

Complexity

Conditions 12
Paths 387

Size

Total Lines 66
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 12
Bugs 5 Features 0
Metric Value
eloc 39
c 12
b 5
f 0
dl 0
loc 66
rs 3.7958
cc 12
nc 387
nop 8

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace Blacklight;
4
5
use App\Models\Category;
6
use App\Models\Release;
7
use App\Models\Settings;
8
use Elasticsearch;
9
use Elasticsearch\Common\Exceptions\Missing404Exception;
10
use Illuminate\Database\Eloquent\Collection;
11
use Illuminate\Support\Arr;
12
use Illuminate\Support\Facades\Cache;
13
use Illuminate\Support\Facades\DB;
14
use Illuminate\Support\Facades\File;
15
16
/**
17
 * Class Releases.
18
 */
19
class Releases extends Release
20
{
21
    // RAR/ZIP Password indicator.
22
    public const PASSWD_NONE = 0; // No password.
23
24
    public const PASSWD_RAR = 1; // Definitely passworded.
25
26
    public int $passwordStatus;
27
28
    private ManticoreSearch $manticoreSearch;
29
30
    private ElasticSearchSiteSearch $elasticSearch;
31
32
    public function __construct()
33
    {
34
        parent::__construct();
35
        $this->manticoreSearch = new ManticoreSearch;
36
        $this->elasticSearch = new ElasticSearchSiteSearch;
37
    }
38
39
    /**
40
     * Used for Browse results.
41
     *
42
     * @return Collection|mixed
43
     */
44
    public function getBrowseRange($page, $cat, $start, $num, $orderBy, int $maxAge = -1, array $excludedCats = [], int|string $groupName = -1, int $minSize = 0): mixed
45
    {
46
        $page = max(1, $page);
47
        $start = max(0, $start);
48
49
        $orderBy = $this->getBrowseOrder($orderBy);
50
51
        $query = self::query()
52
            ->with(['group', 'category', 'category.parent', 'video', 'video.episode', 'videoData', 'nfo', 'failed'])
53
            ->where('nzbstatus', NZB::NZB_ADDED)
54
            ->where('passwordstatus', $this->showPasswords());
55
56
        if ($cat) {
57
            $categories = Category::getCategorySearch($cat, null, true);
58
            // If categories is empty, we don't want to return anything.
59
            if ($categories !== null) {
60
                // if we have more than one category, we need to use whereIn
61
                if (count(Arr::wrap($categories)) > 1) {
62
                    $query->whereIn('categories_id', $categories);
63
                } else {
64
                    $query->where('categories_id', $categories);
65
                }
66
            }
67
        }
68
69
        if ($maxAge > 0) {
70
            $query->where('postdate', '>', now()->subDays($maxAge));
71
        }
72
73
        if (! empty($excludedCats)) {
74
            $query->whereNotIn('categories_id', $excludedCats);
75
        }
76
77
        if ($groupName !== -1) {
78
            $query->whereHas('group', function ($q) use ($groupName) {
79
                $q->where('name', $groupName);
80
            });
81
        }
82
83
        if ($minSize > 0) {
84
            $query->where('size', '>=', $minSize);
85
        }
86
87
        $query->orderBy($orderBy[0], $orderBy[1])
88
            ->skip($start)
89
            ->take($num);
90
        $releases = Cache::get(md5($query->toRawSql().$page));
0 ignored issues
show
Bug introduced by
Are you sure $query->toRawSql() of type Illuminate\Database\Eloquent\Builder|mixed|string 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

90
        $releases = Cache::get(md5(/** @scrutinizer ignore-type */ $query->toRawSql().$page));
Loading history...
91
        if ($releases !== null) {
92
            return $releases;
93
        }
94
95
        $sql = $query->get();
96
        if ($sql->isNotEmpty()) {
97
            $possibleRows = $sql->count();
98
            $sql[0]->_totalcount = $sql[0]->_totalrows = $possibleRows;
99
        }
100
101
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium'));
102
        Cache::put(md5($query->toRawSql().$page), $sql, $expiresAt);
103
104
        return $sql;
105
    }
106
107
    public function showPasswords($builder = false): string
108
    {
109
        $show = (int) Settings::settingValue('showpasswordedrelease');
110
        $setting = $show ?? 0;
111
112
        return match ($setting) {
113
            1 => $builder === true ? self::PASSWD_RAR : '<= '.self::PASSWD_RAR,
114
            default => $builder === true ? self::PASSWD_NONE : '= '.self::PASSWD_NONE,
115
        };
116
    }
117
118
    /**
119
     * Use to order releases on site.
120
     */
121
    public function getBrowseOrder(array|string $orderBy): array
122
    {
123
        $orderArr = explode('_', ($orderBy === '' ? 'posted_desc' : $orderBy));
0 ignored issues
show
introduced by
The condition $orderBy === '' is always false.
Loading history...
Bug introduced by
$orderBy === '' ? 'posted_desc' : $orderBy of type array is incompatible with the type string expected by parameter $string of explode(). ( Ignorable by Annotation )

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

123
        $orderArr = explode('_', /** @scrutinizer ignore-type */ ($orderBy === '' ? 'posted_desc' : $orderBy));
Loading history...
124
        $orderField = match ($orderArr[0]) {
125
            'cat' => 'categories_id',
126
            'name' => 'searchname',
127
            'size' => 'size',
128
            'files' => 'totalpart',
129
            'stats' => 'grabs',
130
            default => 'postdate',
131
        };
132
133
        return [$orderField, isset($orderArr[1]) && preg_match('/^(asc|desc)$/i', $orderArr[1]) ? $orderArr[1] : 'desc'];
134
    }
135
136
    /**
137
     * Return ordering types usable on site.
138
     *
139
     * @return string[]
140
     */
141
    public function getBrowseOrdering(): array
142
    {
143
        return [
144
            'name_asc',
145
            'name_desc',
146
            'cat_asc',
147
            'cat_desc',
148
            'posted_asc',
149
            'posted_desc',
150
            'size_asc',
151
            'size_desc',
152
            'files_asc',
153
            'files_desc',
154
            'stats_asc',
155
            'stats_desc',
156
        ];
157
    }
158
159
    /**
160
     * @return Collection|\Illuminate\Support\Collection|Release[]
161
     */
162
    public function getForExport(string $postFrom = '', string $postTo = '', string $groupID = ''): Collection|array|\Illuminate\Support\Collection
163
    {
164
        $query = self::query()
165
            ->where('r.nzbstatus', NZB::NZB_ADDED)
166
            ->select(['r.searchname', 'r.guid', 'g.name as gname', DB::raw("CONCAT(cp.title,'_',c.title) AS catName")])
167
            ->from('releases as r')
0 ignored issues
show
Bug introduced by
'releases as r' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $table of Illuminate\Database\Query\Builder::from(). ( Ignorable by Annotation )

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

167
            ->from(/** @scrutinizer ignore-type */ 'releases as r')
Loading history...
168
            ->leftJoin('categories as c', 'c.id', '=', 'r.categories_id')
169
            ->leftJoin('root_categories as cp', 'cp.id', '=', 'c.root_categories_id')
170
            ->leftJoin('usenet_groups as g', 'g.id', '=', 'r.groups_id');
171
172
        if ($groupID !== '') {
173
            $query->where('r.groups_id', $groupID);
174
        }
175
176
        if ($postFrom !== '') {
177
            $dateParts = explode('/', $postFrom);
178
            if (\count($dateParts) === 3) {
179
                $query->where('r.postdate', '>', $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0].'00:00:00');
180
            }
181
        }
182
183
        if ($postTo !== '') {
184
            $dateParts = explode('/', $postTo);
185
            if (\count($dateParts) === 3) {
186
                $query->where('r.postdate', '<', $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0].'23:59:59');
187
            }
188
        }
189
190
        return $query->get();
191
    }
192
193
    /**
194
     * @return mixed|string
195
     */
196
    public function getEarliestUsenetPostDate(): mixed
197
    {
198
        $row = self::query()->selectRaw("DATE_FORMAT(min(postdate), '%d/%m/%Y') AS postdate")->first();
199
200
        return $row === null ? '01/01/2014' : $row['postdate'];
201
    }
202
203
    /**
204
     * @return mixed|string
205
     */
206
    public function getLatestUsenetPostDate(): mixed
207
    {
208
        $row = self::query()->selectRaw("DATE_FORMAT(max(postdate), '%d/%m/%Y') AS postdate")->first();
209
210
        return $row === null ? '01/01/2014' : $row['postdate'];
211
    }
212
213
    public function getReleasedGroupsForSelect(bool $blnIncludeAll = true): array
214
    {
215
        $groups = self::query()
216
            ->selectRaw('DISTINCT g.id, g.name')
217
            ->leftJoin('usenet_groups as g', 'g.id', '=', 'releases.groups_id')
218
            ->get();
219
        $temp_array = [];
220
221
        if ($blnIncludeAll) {
222
            $temp_array[-1] = '--All Groups--';
223
        }
224
225
        foreach ($groups as $group) {
226
            $temp_array[$group['id']] = $group['name'];
227
        }
228
229
        return $temp_array;
230
    }
231
232
    /**
233
     * @return Collection|mixed
234
     */
235
    public function getShowsRange($userShows, $offset, $limit, $orderBy, int $maxAge = -1, array $excludedCats = []): mixed
236
    {
237
        $orderBy = $this->getBrowseOrder($orderBy);
238
239
        $query = self::query()
240
            ->with(['group', 'category', 'category.parent', 'video', 'video.episode'])
241
            ->where('nzbstatus', NZB::NZB_ADDED)
242
            ->where('passwordstatus', $this->showPasswords(true))
243
            ->whereBetween('categories_id', [Category::TV_ROOT, Category::TV_OTHER])
244
            ->when($maxAge > 0, function ($q) use ($maxAge) {
245
                $q->where('postdate', '>', now()->subDays($maxAge));
246
            })
247
            ->when(! empty($excludedCats), function ($q) use ($excludedCats) {
248
                $q->whereNotIn('categories_id', $excludedCats);
249
            })
250
            ->whereRaw($this->uSQL($userShows, 'videos_id'))
251
            ->orderBy($orderBy[0], $orderBy[1])
252
            ->offset($offset)
253
            ->limit($limit);
254
255
        $cacheKey = md5($query->toRawSql());
0 ignored issues
show
Bug introduced by
It seems like $query->toRawSql() can also be of type Illuminate\Database\Eloquent\Builder and Illuminate\Database\Eloq...gHasThroughRelationship and Illuminate\Database\Query\Builder; however, parameter $string of md5() 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

255
        $cacheKey = md5(/** @scrutinizer ignore-type */ $query->toRawSql());
Loading history...
256
        $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium'));
257
258
        $releases = Cache::get($cacheKey);
259
        if ($releases !== null) {
260
            return $releases;
261
        }
262
263
        $releases = $query->get();
264
265
        if ($releases->isNotEmpty()) {
266
            $releases[0]->_totalrows = $query->count();
267
        }
268
269
        Cache::put($cacheKey, $releases, $cacheTTL);
270
271
        return $releases;
272
    }
273
274
    public function getShowsCount($userShows, int $maxAge = -1, array $excludedCats = []): int
275
    {
276
        return $this->getPagerCount(
277
            sprintf(
278
                'SELECT r.id
279
				FROM releases r
280
				WHERE %s %s
281
				AND r.nzbstatus = %d
282
				AND r.categories_id BETWEEN %d AND %d
283
				AND r.passwordstatus %s
284
				%s',
285
                $this->uSQL($userShows, 'videos_id'),
286
                (\count($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''),
287
                NZB::NZB_ADDED,
288
                Category::TV_ROOT,
289
                Category::TV_OTHER,
290
                $this->showPasswords(),
291
                ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : '')
292
            )
293
        );
294
    }
295
296
    /**
297
     * @throws \Exception
298
     */
299
    public function deleteMultiple(int|array|string $list): void
300
    {
301
        $list = (array) $list;
302
303
        $nzb = new NZB;
304
        $releaseImage = new ReleaseImage;
305
306
        foreach ($list as $identifier) {
307
            $this->deleteSingle(['g' => $identifier, 'i' => false], $nzb, $releaseImage);
308
        }
309
    }
310
311
    /**
312
     * Deletes a single release by GUID, and all the corresponding files.
313
     *
314
     * @param  array  $identifiers  ['g' => Release GUID(mandatory), 'id => ReleaseID(optional, pass
315
     *                              false)]
316
     *
317
     * @throws \Exception
318
     */
319
    public function deleteSingle(array $identifiers, NZB $nzb, ReleaseImage $releaseImage): void
320
    {
321
        // Delete NZB from disk.
322
        $nzbPath = $nzb->NZBPath($identifiers['g']);
323
        if (! empty($nzbPath)) {
324
            File::delete($nzbPath);
325
        }
326
327
        // Delete images.
328
        $releaseImage->delete($identifiers['g']);
329
330
        if (config('nntmux.elasticsearch_enabled') === true) {
331
            if ($identifiers['i'] === false) {
332
                $identifiers['i'] = Release::query()->where('guid', $identifiers['g'])->first(['id']);
333
                if ($identifiers['i'] !== null) {
334
                    $identifiers['i'] = $identifiers['i']['id'];
335
                }
336
            }
337
            if ($identifiers['i'] !== null) {
338
                $params = [
339
                    'index' => 'releases',
340
                    'id' => $identifiers['i'],
341
                ];
342
343
                try {
344
                    Elasticsearch::delete($params);
345
                } catch (Missing404Exception $e) {
346
                    // we do nothing here just catch the error, we don't care if release is missing from ES, we are deleting it anyway
347
                }
348
            }
349
        } else {
350
            // Delete from sphinx.
351
            $this->manticoreSearch->deleteRelease($identifiers);
352
        }
353
354
        // Delete from DB.
355
        self::whereGuid($identifiers['g'])->delete();
356
    }
357
358
    /**
359
     * @return bool|int
360
     */
361
    public function updateMulti($guids, $category, $grabs, $videoId, $episodeId, $anidbId, $imdbId)
362
    {
363
        if (! \is_array($guids) || \count($guids) < 1) {
364
            return false;
365
        }
366
367
        $update = [
368
            'categories_id' => $category === -1 ? 'categories_id' : $category,
369
            'grabs' => $grabs,
370
            'videos_id' => $videoId,
371
            'tv_episodes_id' => $episodeId,
372
            'anidbid' => $anidbId,
373
            'imdbid' => $imdbId,
374
        ];
375
376
        return self::query()->whereIn('guid', $guids)->update($update);
377
    }
378
379
    /**
380
     * Creates part of a query for some functions.
381
     */
382
    public function uSQL(Collection|array $userQuery, string $type): string
383
    {
384
        $sql = '(1=2 ';
385
        foreach ($userQuery as $query) {
386
            $sql .= sprintf('OR (r.%s = %d', $type, $query->$type);
387
            if (! empty($query->categories)) {
388
                $catsArr = explode('|', $query->categories);
389
                if (\count($catsArr) > 1) {
390
                    $sql .= sprintf(' AND r.categories_id IN (%s)', implode(',', $catsArr));
391
                } else {
392
                    $sql .= sprintf(' AND r.categories_id = %d', $catsArr[0]);
393
                }
394
            }
395
            $sql .= ') ';
396
        }
397
        $sql .= ') ';
398
399
        return $sql;
400
    }
401
402
    /**
403
     * Function for searching on the site (by subject, searchname or advanced).
404
     *
405
     * @return array|Collection|mixed
406
     */
407
    public function search(array $searchArr, $groupName, $sizeFrom, $sizeTo, $daysNew, $daysOld, int $offset = 0, int $limit = 1000, array|string $orderBy = '', int $maxAge = -1, array $excludedCats = [], string $type = 'basic', array $cat = [-1], int $minSize = 0): mixed
408
    {
409
        $sizeRange = [
410
            1 => 1,
411
            2 => 2.5,
412
            3 => 5,
413
            4 => 10,
414
            5 => 20,
415
            6 => 30,
416
            7 => 40,
417
            8 => 80,
418
            9 => 160,
419
            10 => 320,
420
            11 => 640,
421
        ];
422
423
        if ($orderBy === '') {
424
            $orderBy = ['postdate', 'desc'];
425
        } else {
426
            $orderBy = $this->getBrowseOrder($orderBy);
427
        }
428
429
        $searchFields = Arr::where($searchArr, static function ($value) {
430
            return $value !== -1;
431
        });
432
433
        $phrases = array_values($searchFields);
434
435
        if (config('nntmux.elasticsearch_enabled') === true) {
436
            $searchResult = $this->elasticSearch->indexSearch($phrases, $limit);
437
        } else {
438
            $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', '', [], $searchFields);
439
            if (! empty($searchResult)) {
440
                $searchResult = Arr::wrap(Arr::get($searchResult, 'id'));
441
            }
442
        }
443
444
        if (count($searchResult) === 0) {
445
            return collect();
446
        }
447
448
        $query = self::query()
449
            ->with(['group', 'category', 'category.parent', 'video', 'video.episode', 'nfo', 'failed'])
450
            ->where('nzbstatus', NZB::NZB_ADDED)
451
            ->where('passwordstatus', $this->showPasswords(true))
452
            ->whereIn('id', $searchResult);
453
454
        if ($type === 'basic') {
455
            $categories = Category::getCategorySearch($cat, null, true);
456
            if ($categories !== null) {
457
                $query->whereIn('categories_id', Arr::wrap($categories));
458
            }
459
        } elseif ($type === 'advanced' && (int) $cat[0] !== -1) {
460
            $query->where('categories_id', $cat[0]);
461
        }
462
463
        if ($maxAge > 0) {
464
            $query->where('postdate', '>', now()->subDays($maxAge));
465
        }
466
467
        if (! empty($excludedCats)) {
468
            $query->whereNotIn('categories_id', $excludedCats);
469
        }
470
471
        if ((int) $groupName !== -1) {
472
            $query->whereHas('group', function ($q) use ($groupName) {
473
                $q->where('name', $groupName);
474
            });
475
        }
476
477
        if ($sizeFrom > 0 && array_key_exists($sizeFrom, $sizeRange)) {
478
            $query->where('size', '>', 104857600 * (int) $sizeRange[$sizeFrom]);
479
        }
480
481
        if ($sizeTo > 0 && array_key_exists($sizeTo, $sizeRange)) {
482
            $query->where('size', '<', 104857600 * (int) $sizeRange[$sizeTo]);
483
        }
484
485
        if ($daysNew !== -1) {
486
            $query->where('postdate', '<', now()->subDays($daysNew));
487
        }
488
489
        if ($daysOld !== -1) {
490
            $query->where('postdate', '>', now()->subDays($daysOld));
491
        }
492
493
        if ($minSize > 0) {
494
            $query->where('size', '>=', $minSize);
495
        }
496
497
        $query->orderBy($orderBy[0], $orderBy[1])
498
            ->offset($offset)
499
            ->limit($limit);
500
501
        $cacheKey = md5($query->toRawSql());
0 ignored issues
show
Bug introduced by
It seems like $query->toRawSql() can also be of type Illuminate\Database\Eloquent\Builder and Illuminate\Database\Query\Builder; however, parameter $string of md5() 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

501
        $cacheKey = md5(/** @scrutinizer ignore-type */ $query->toRawSql());
Loading history...
502
        $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium'));
503
504
        $releases = Cache::get($cacheKey);
505
        if ($releases !== null) {
506
            return $releases;
507
        }
508
509
        $releases = $query->get();
510
511
        if ($releases->isNotEmpty()) {
512
            $releases[0]->_totalrows = $query->count();
513
        }
514
515
        Cache::put($cacheKey, $releases, $cacheTTL);
516
517
        return $releases;
518
    }
519
520
    /**
521
     * Search function for API.
522
     *
523
     * @return Collection|mixed
524
     */
525
    public function apiSearch($searchName, $groupName, int $offset = 0, int $limit = 1000, int $maxAge = -1, array $excludedCats = [], array $cat = [-1], int $minSize = 0): mixed
526
    {
527
        $query = self::query()
528
            ->with(['video', 'video.episode', 'movieinfo', 'group', 'category', 'category.parent'])
529
            ->where('nzbstatus', NZB::NZB_ADDED)
530
            ->where('passwordstatus', $this->showPasswords(true));
531
532
        if ($searchName !== -1) {
533
            if (config('nntmux.elasticsearch_enabled') === true) {
534
                $searchResult = $this->elasticSearch->indexSearchApi($searchName, $limit);
535
            } else {
536
                $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $searchName, ['searchname']);
537
                if (! empty($searchResult)) {
538
                    $searchResult = Arr::wrap(Arr::get($searchResult, 'id'));
539
                }
540
            }
541
            if (count($searchResult) !== 0) {
542
                $query->whereIn('id', $searchResult);
543
            } else {
544
                return collect();
545
            }
546
        }
547
548
        if ($maxAge > 0) {
549
            $query->where('postdate', '>', now()->subDays($maxAge));
550
        }
551
552
        if (! empty($excludedCats)) {
553
            $query->whereNotIn('categories_id', $excludedCats);
554
        }
555
556
        if ((int) $groupName !== -1) {
557
            $query->whereHas('group', function ($q) use ($groupName) {
558
                $q->where('name', $groupName);
559
            });
560
        }
561
562
        if ($cat !== [-1]) {
563
            $query->whereIn('categories_id', $cat);
564
        }
565
566
        if ($minSize > 0) {
567
            $query->where('size', '>=', $minSize);
568
        }
569
570
        $query->orderBy('postdate', 'desc')
571
            ->offset($offset)
572
            ->limit($limit);
573
574
        $cacheKey = md5($query->toRawSql());
0 ignored issues
show
Bug introduced by
It seems like $query->toRawSql() can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $string of md5() 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

574
        $cacheKey = md5(/** @scrutinizer ignore-type */ $query->toRawSql());
Loading history...
575
        $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium'));
576
577
        $releases = Cache::get($cacheKey);
578
        if ($releases !== null) {
579
            return $releases;
580
        }
581
582
        $releases = $query->get();
583
584
        if ($releases->isNotEmpty()) {
585
            $releases[0]->_totalrows = $query->count();
586
        }
587
588
        Cache::put($cacheKey, $releases, $cacheTTL);
589
590
        return $releases;
591
    }
592
593
    /**
594
     * Search for TV shows via API.
595
     *
596
     * @return array|\Illuminate\Cache\|Collection|\Illuminate\Support\Collection|mixed
597
     */
598
    public function tvSearch(array $siteIdArr = [], string $series = '', string $episode = '', string $airDate = '', int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, int $minSize = 0, array $excludedCategories = []): mixed
599
    {
600
        $siteSQL = [];
601
        $showSql = '';
602
        foreach ($siteIdArr as $column => $Id) {
603
            if ($Id > 0) {
604
                $siteSQL[] = sprintf('v.%s = %d', $column, $Id);
605
            }
606
        }
607
608
        if (\count($siteSQL) > 0) {
609
            // If we have show info, find the Episode ID/Video ID first to avoid table scans
610
            $showQry = sprintf(
611
                "
612
				SELECT
613
					v.id AS video,
614
					GROUP_CONCAT(tve.id SEPARATOR ',') AS episodes
615
				FROM videos v
616
				LEFT JOIN tv_episodes tve ON v.id = tve.videos_id
617
				WHERE (%s) %s %s %s
618
				GROUP BY v.id
619
				LIMIT 1",
620
                implode(' OR ', $siteSQL),
621
                ($series !== '' ? sprintf('AND tve.series = %d', (int) preg_replace('/^s0*/i', '', $series)) : ''),
622
                ($episode !== '' ? sprintf('AND tve.episode = %d', (int) preg_replace('/^e0*/i', '', $episode)) : ''),
623
                ($airDate !== '' ? sprintf('AND DATE(tve.firstaired) = %s', escapeString($airDate)) : '')
624
            );
625
626
            $show = self::fromQuery($showQry);
627
628
            if ($show->isNotEmpty()) {
629
                if ((! empty($episode) && ! empty($series)) && $show[0]->episodes !== '') {
630
                    $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series;
631
                } elseif (! empty($episode) && $show[0]->episodes !== '') {
632
                    $showSql = sprintf('AND r.tv_episodes_id IN (%s)', $show[0]->episodes);
633
                } elseif (! empty($series) && empty($episode)) {
634
                    // If $series is set but episode is not, return Season Packs and Episodes
635
                    $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series;
636
                }
637
                if ($show[0]->video > 0) {
638
                    $showSql .= ' AND r.videos_id = '.$show[0]->video;
639
                }
640
            } else {
641
                // If we were passed Site ID Info and no match was found, do not run the query
642
                return [];
643
            }
644
        }
645
646
        // If $name is set it is a fallback search, add available SxxExx/airdate info to the query
647
        if (! empty($name) && $showSql === '') {
648
            if (! empty($series) && (int) $series < 1900) {
649
                $name .= sprintf(' S%s', str_pad($series, 2, '0', STR_PAD_LEFT));
650
                if (! empty($episode) && ! str_contains($episode, '/')) {
651
                    $name .= sprintf('E%s', str_pad($episode, 2, '0', STR_PAD_LEFT));
652
                }
653
                // If season is not empty but episode is, add a wildcard to the search
654
                if (empty($episode)) {
655
                    $name .= '*';
656
                }
657
            } elseif (! empty($airDate)) {
658
                $name .= sprintf(' %s', str_replace(['/', '-', '.', '_'], ' ', $airDate));
659
            }
660
        }
661
        if (! empty($name)) {
662
            if (config('nntmux.elasticsearch_enabled') === true) {
663
                $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit);
664
            } else {
665
                $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']);
666
                if (! empty($searchResult)) {
667
                    $searchResult = Arr::wrap(Arr::get($searchResult, 'id'));
668
                }
669
            }
670
671
            if (empty($searchResult)) {
672
                return collect();
673
            }
674
        }
675
        $whereSql = sprintf(
676
            'WHERE r.nzbstatus = %d
677
			AND r.passwordstatus %s
678
			%s %s %s %s %s %s',
679
            NZB::NZB_ADDED,
680
            $this->showPasswords(),
681
            $showSql,
682
            (! empty($name) && ! empty($searchResult)) ? 'AND r.id IN ('.implode(',', $searchResult).')' : '',
683
            Category::getCategorySearch($cat, 'tv'),
0 ignored issues
show
Bug introduced by
It seems like App\Models\Category::get...egorySearch($cat, 'tv') can also be of type array; however, parameter $values of sprintf() does only seem to accept double|integer|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

683
            /** @scrutinizer ignore-type */ Category::getCategorySearch($cat, 'tv'),
Loading history...
684
            $maxAge > 0 ? sprintf('AND r.postdate > NOW() - INTERVAL %d DAY', $maxAge) : '',
685
            $minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : '',
686
            ! empty($excludedCategories) ? sprintf('AND r.categories_id NOT IN('.implode(',', $excludedCategories).')') : ''
687
        );
688
        $baseSql = sprintf(
689
            "SELECT r.searchname, r.guid, r.postdate, r.groups_id, r.categories_id, r.size, r.totalpart, r.fromname, r.passwordstatus, r.grabs, r.comments, r.adddate, r.videos_id, r.tv_episodes_id, r.haspreview, r.jpgstatus,
690
				v.title, v.countries_id, v.started, v.tvdb, v.trakt,
691
					v.imdb, v.tmdb, v.tvmaze, v.tvrage, v.source,
692
				tvi.summary, tvi.publisher, tvi.image,
693
				tve.series, tve.episode, tve.se_complete, tve.title, tve.firstaired, tve.summary, cp.title AS parent_category, c.title AS sub_category,
694
				CONCAT(cp.title, ' > ', c.title) AS category_name,
695
				g.name AS group_name,
696
				rn.releases_id AS nfoid,
697
				re.releases_id AS reid
698
			FROM releases r
699
			LEFT OUTER JOIN videos v ON r.videos_id = v.id AND v.type = 0
700
			LEFT OUTER JOIN tv_info tvi ON v.id = tvi.videos_id
701
			LEFT OUTER JOIN tv_episodes tve ON r.tv_episodes_id = tve.id
702
			LEFT JOIN categories c ON c.id = r.categories_id
703
			LEFT JOIN root_categories cp ON cp.id = c.root_categories_id
704
			LEFT JOIN usenet_groups g ON g.id = r.groups_id
705
			LEFT OUTER JOIN video_data re ON re.releases_id = r.id
706
			LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id
707
			%s",
708
            $whereSql
709
        );
710
        $sql = sprintf(
711
            '%s
712
			ORDER BY postdate DESC
713
			LIMIT %d OFFSET %d',
714
            $baseSql,
715
            $limit,
716
            $offset
717
        );
718
        $releases = Cache::get(md5($sql));
719
        if ($releases !== null) {
720
            return $releases;
721
        }
722
        $releases = ((! empty($name) && ! empty($searchResult)) || empty($name)) ? self::fromQuery($sql) : [];
723
        if (count($releases) !== 0 && $releases->isNotEmpty()) {
0 ignored issues
show
Bug introduced by
It seems like $releases can also be of type Illuminate\Database\Eloq...gHasThroughRelationship; however, parameter $value of count() does only seem to accept Countable|array, 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

723
        if (count(/** @scrutinizer ignore-type */ $releases) !== 0 && $releases->isNotEmpty()) {
Loading history...
724
            $releases[0]->_totalrows = $this->getPagerCount(
725
                preg_replace('#LEFT(\s+OUTER)?\s+JOIN\s+(?!tv_episodes)\s+.*ON.*=.*\n#i', ' ', $baseSql)
726
            );
727
        }
728
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium'));
729
        Cache::put(md5($sql), $releases, $expiresAt);
730
731
        return $releases;
732
    }
733
734
    /**
735
     * Search TV Shows via APIv2.
736
     *
737
     *
738
     * @return Collection|mixed
739
     */
740
    public function apiTvSearch(array $siteIdArr = [], string $series = '', string $episode = '', string $airDate = '', int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, int $minSize = 0, array $excludedCategories = []): mixed
741
    {
742
        $siteSQL = [];
743
        $showSql = '';
744
        foreach ($siteIdArr as $column => $Id) {
745
            if ($Id > 0) {
746
                $siteSQL[] = sprintf('v.%s = %d', $column, $Id);
747
            }
748
        }
749
750
        if (\count($siteSQL) > 0) {
751
            // If we have show info, find the Episode ID/Video ID first to avoid table scans
752
            $showQry = sprintf(
753
                "
754
				SELECT
755
					v.id AS video,
756
					GROUP_CONCAT(tve.id SEPARATOR ',') AS episodes
757
				FROM videos v
758
				LEFT JOIN tv_episodes tve ON v.id = tve.videos_id
759
				WHERE (%s) %s %s %s
760
				GROUP BY v.id
761
				LIMIT 1",
762
                implode(' OR ', $siteSQL),
763
                ($series !== '' ? sprintf('AND tve.series = %d', (int) preg_replace('/^s0*/i', '', $series)) : ''),
764
                ($episode !== '' ? sprintf('AND tve.episode = %d', (int) preg_replace('/^e0*/i', '', $episode)) : ''),
765
                ($airDate !== '' ? sprintf('AND DATE(tve.firstaired) = %s', escapeString($airDate)) : '')
766
            );
767
768
            $show = self::fromQuery($showQry);
769
            if ($show->isNotEmpty()) {
770
                if ((! empty($episode) && ! empty($series)) && $show[0]->episodes !== '') {
771
                    $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series;
772
                } elseif (! empty($episode) && $show[0]->episodes !== '') {
773
                    $showSql = sprintf('AND r.tv_episodes_id IN (%s)', $show[0]->episodes);
774
                } elseif (! empty($series) && empty($episode)) {
775
                    // If $series is set but episode is not, return Season Packs and Episodes
776
                    $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series;
777
                }
778
                if ($show[0]->video > 0) {
779
                    $showSql .= ' AND r.videos_id = '.$show[0]->video;
780
                }
781
            } else {
782
                // If we were passed Site ID Info and no match was found, do not run the query
783
                return [];
784
            }
785
        }
786
        // If $name is set it is a fallback search, add available SxxExx/airdate info to the query
787
        if (! empty($name) && $showSql === '') {
788
            if (! empty($series) && (int) $series < 1900) {
789
                $name .= sprintf(' S%s', str_pad($series, 2, '0', STR_PAD_LEFT));
790
                if (! empty($episode) && ! str_contains($episode, '/')) {
791
                    $name .= sprintf('E%s', str_pad($episode, 2, '0', STR_PAD_LEFT));
792
                }
793
                // If season is not empty but episode is, add a wildcard to the search
794
                if (empty($episode)) {
795
                    $name .= '*';
796
                }
797
            } elseif (! empty($airDate)) {
798
                $name .= sprintf(' %s', str_replace(['/', '-', '.', '_'], ' ', $airDate));
799
            }
800
        }
801
        if (! empty($name)) {
802
            if (config('nntmux.elasticsearch_enabled') === true) {
803
                $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit);
804
            } else {
805
                $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']);
806
                if (! empty($searchResult)) {
807
                    $searchResult = Arr::wrap(Arr::get($searchResult, 'id'));
808
                }
809
            }
810
811
            if (empty($searchResult)) {
812
                return collect();
813
            }
814
        }
815
        $whereSql = sprintf(
816
            'WHERE r.nzbstatus = %d
817
			AND r.passwordstatus %s
818
			%s %s %s %s %s %s',
819
            NZB::NZB_ADDED,
820
            $this->showPasswords(),
821
            $showSql,
822
            (! empty($searchResult) ? 'AND r.id IN ('.implode(',', $searchResult).')' : ''),
823
            Category::getCategorySearch($cat, 'tv'),
0 ignored issues
show
Bug introduced by
It seems like App\Models\Category::get...egorySearch($cat, 'tv') can also be of type array; however, parameter $values of sprintf() does only seem to accept double|integer|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

823
            /** @scrutinizer ignore-type */ Category::getCategorySearch($cat, 'tv'),
Loading history...
824
            ($maxAge > 0 ? sprintf('AND r.postdate > NOW() - INTERVAL %d DAY', $maxAge) : ''),
825
            ($minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : ''),
826
            ! empty($excludedCategories) ? sprintf('AND r.categories_id NOT IN('.implode(',', $excludedCategories).')') : ''
827
        );
828
        $baseSql = sprintf(
829
            "SELECT r.searchname, r.guid, r.postdate, r.groups_id, r.categories_id, r.size, r.totalpart, r.fromname, r.passwordstatus, r.grabs, r.comments, r.adddate, r.tv_episodes_id, r.haspreview, r.jpgstatus,
830
				v.title, v.type, v.tvdb, v.trakt,v.imdb, v.tmdb, v.tvmaze, v.tvrage,
831
				tve.series, tve.episode, tve.se_complete, tve.title, tve.firstaired, cp.title AS parent_category, c.title AS sub_category,
832
				CONCAT(cp.title, ' > ', c.title) AS category_name,
833
				g.name AS group_name
834
			FROM releases r
835
			LEFT OUTER JOIN videos v ON r.videos_id = v.id AND v.type = 0
836
			LEFT OUTER JOIN tv_info tvi ON v.id = tvi.videos_id
837
			LEFT OUTER JOIN tv_episodes tve ON r.tv_episodes_id = tve.id
838
			LEFT JOIN categories c ON c.id = r.categories_id
839
			LEFT JOIN root_categories cp ON cp.id = c.root_categories_id
840
			LEFT JOIN usenet_groups g ON g.id = r.groups_id
841
			%s",
842
            $whereSql
843
        );
844
        $sql = sprintf(
845
            '%s
846
			ORDER BY postdate DESC
847
			LIMIT %d OFFSET %d',
848
            $baseSql,
849
            $limit,
850
            $offset
851
        );
852
        $releases = Cache::get(md5($sql));
853
        if ($releases !== null) {
854
            return $releases;
855
        }
856
        $releases = self::fromQuery($sql);
857
        if ($releases->isNotEmpty()) {
858
            $releases[0]->_totalrows = $this->getPagerCount(
859
                preg_replace('#LEFT(\s+OUTER)?\s+JOIN\s+(?!tv_episodes)\s+.*ON.*=.*\n#i', ' ', $baseSql)
860
            );
861
        }
862
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium'));
863
        Cache::put(md5($sql), $releases, $expiresAt);
864
865
        return $releases;
866
    }
867
868
    /**
869
     * Movies search through API and site.
870
     *
871
     * @return Collection|mixed
872
     */
873
    public function moviesSearch(int $imDbId = -1, int $tmDbId = -1, int $traktId = -1, int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, int $minSize = 0, array $excludedCategories = []): mixed
874
    {
875
        $cacheKey = md5(json_encode(func_get_args()));
876
        $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium'));
877
878
        return Cache::remember($cacheKey, $cacheTTL, function () use ($imDbId, $tmDbId, $traktId, $offset, $limit, $name, $cat, $maxAge, $minSize, $excludedCategories) {
879
            $query = self::query()
880
                ->with(['movieinfo', 'group', 'category', 'category.parent', 'nfo'])
881
                ->whereBetween('categories_id', [Category::MOVIE_ROOT, Category::MOVIE_OTHER])
882
                ->where('nzbstatus', NZB::NZB_ADDED)
883
                ->where('passwordstatus', $this->showPasswords());
884
885
            if (! empty($name)) {
886
                if (config('nntmux.elasticsearch_enabled') === true) {
887
                    $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit);
888
                } else {
889
                    $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']);
890
                    if (! empty($searchResult)) {
891
                        $searchResult = Arr::wrap(Arr::get($searchResult, 'id'));
892
                    }
893
                }
894
895
                if (count($searchResult) === 0) {
896
                    return collect();
897
                }
898
899
                $query->whereIn('id', $searchResult);
900
            }
901
902
            if ($imDbId !== -1 && is_numeric($imDbId)) {
903
                $query->whereHas('movieinfo', function ($q) use ($imDbId) {
904
                    $q->where('imdbid', $imDbId);
905
                });
906
            }
907
908
            if ($tmDbId !== -1 && is_numeric($tmDbId)) {
909
                $query->whereHas('movieinfo', function ($q) use ($tmDbId) {
910
                    $q->where('tmdbid', $tmDbId);
911
                });
912
            }
913
914
            if ($traktId !== -1 && is_numeric($traktId)) {
915
                $query->whereHas('movieinfo', function ($q) use ($traktId) {
916
                    $q->where('traktid', $traktId);
917
                });
918
            }
919
920
            if (! empty($excludedCategories)) {
921
                $query->whereNotIn('categories_id', $excludedCategories);
922
            }
923
924
            if ($cat !== [-1]) {
925
                $query->whereIn('categories_id', $cat);
926
            }
927
928
            if ($maxAge > 0) {
929
                $query->where('postdate', '>', now()->subDays($maxAge));
930
            }
931
932
            if ($minSize > 0) {
933
                $query->where('size', '>=', $minSize);
934
            }
935
936
            $totalRows = $query->count();
937
938
            $releases = $query->orderBy('postdate', 'desc')
0 ignored issues
show
Bug introduced by
'postdate' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

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

938
            $releases = $query->orderBy(/** @scrutinizer ignore-type */ 'postdate', 'desc')
Loading history...
939
                ->offset($offset)
940
                ->limit($limit)
941
                ->get();
942
943
            if ($releases->isNotEmpty()) {
944
                $releases[0]->_totalrows = $totalRows;
945
            }
946
947
            return $releases;
948
        });
949
    }
950
951
    public function searchSimilar($currentID, $name, array $excludedCats = []): bool|array
952
    {
953
        // Get the category for the parent of this release.
954
        $ret = false;
955
        $currRow = self::getCatByRelId($currentID);
956
        if ($currRow !== null) {
957
            $catRow = Category::find($currRow['categories_id']);
958
            $parentCat = $catRow !== null ? $catRow['root_categories_id'] : null;
959
960
            if ($parentCat === null) {
961
                return $ret;
962
            }
963
964
            $results = $this->search(['searchname' => getSimilarName($name)], -1, '', '', -1, -1, 0, config('nntmux.items_per_page'), '', -1, $excludedCats, 'basic', [$parentCat]);
965
            if (! $results) {
966
                return $ret;
967
            }
968
969
            $ret = [];
970
            foreach ($results as $res) {
971
                if ($res['id'] !== $currentID && $res['categoryparentid'] === $parentCat) {
972
                    $ret[] = $res;
973
                }
974
            }
975
        }
976
977
        return $ret;
978
    }
979
980
    /**
981
     * Get count of releases for pager.
982
     *
983
     * @param  string  $query  The query to get the count from.
984
     */
985
    private function getPagerCount(string $query): int
986
    {
987
        $queryBuilder = DB::table(DB::raw('('.preg_replace(
988
            '/SELECT.+?FROM\s+releases/is',
989
            'SELECT r.id FROM releases',
990
            $query
991
        ).' LIMIT '.(int) config('nntmux.max_pager_results').') as z'))
992
            ->selectRaw('COUNT(z.id) as count');
993
994
        $sql = $queryBuilder->toSql();
995
        $count = Cache::get(md5($sql));
996
997
        if ($count !== null) {
998
            return $count;
999
        }
1000
1001
        $result = $queryBuilder->first();
1002
        $count = $result->count ?? 0;
1003
1004
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_short'));
1005
        Cache::put(md5($sql), $count, $expiresAt);
1006
1007
        return $count;
1008
    }
1009
}
1010