Passed
Push — master ( 04f1cf...0048d6 )
by Darko
02:44
created

Releases::moviesSearch()   F

Complexity

Conditions 17
Paths 1539

Size

Total Lines 82
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 13
Bugs 6 Features 0
Metric Value
eloc 46
c 13
b 6
f 0
dl 0
loc 82
rs 1.0499
cc 17
nc 1539
nop 10

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(): string
108
    {
109
        $show = (int) Settings::settingValue('showpasswordedrelease');
110
        $setting = $show ?? 0;
111
112
        return match ($setting) {
113
            1 => '<= '.self::PASSWD_RAR,
114
            default => '= '.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())
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())
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());
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
719
        $releases = Cache::get(md5($sql));
720
        if ($releases !== null) {
721
            return $releases;
722
        }
723
        $releases = ((! empty($name) && ! empty($searchResult)) || empty($name)) ? self::fromQuery($sql) : [];
724
        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

724
        if (count(/** @scrutinizer ignore-type */ $releases) !== 0 && $releases->isNotEmpty()) {
Loading history...
725
            $releases[0]->_totalrows = $this->getPagerCount(
726
                preg_replace('#LEFT(\s+OUTER)?\s+JOIN\s+(?!tv_episodes)\s+.*ON.*=.*\n#i', ' ', $baseSql)
727
            );
728
        }
729
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium'));
730
        Cache::put(md5($sql), $releases, $expiresAt);
731
732
        return $releases;
733
    }
734
735
    /**
736
     * Search TV Shows via APIv2.
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
        $query = Release::query()
743
            ->with(['video', 'video.episode', 'category', 'category.parent', 'group'])
744
            ->where('nzbstatus', NZB::NZB_ADDED)
745
            ->where('passwordstatus', $this->showPasswords())
746
            ->whereIn('categories_id', Category::getCategorySearch($cat, 'tv', true));
747
748
        // Check if siteIdArr contains id key
749
        if (! empty($siteIdArr) && array_key_exists('id', $siteIdArr) && $siteIdArr['id'] > 0) {
750
            $query->where('videos_id', $siteIdArr['id']);
751
        }
752
        if (! empty($series)) {
753
            $query->whereHas('episode', function ($q) use ($series, $episode, $airDate) {
754
                $q->where('series', (int) preg_replace('/^s0*/i', '', $series));
755
                if (! empty($episode)) {
756
                    $q->where('episode', (int) preg_replace('/^e0*/i', '', $episode));
757
                }
758
                if (! empty($airDate)) {
759
                    $q->whereDate('firstaired', $airDate);
760
                }
761
            });
762
        }
763
764
        if (! empty(array_filter($siteIdArr))) {
765
            $query->whereHas('video', function ($q) use ($siteIdArr) {
766
                foreach ($siteIdArr as $column => $id) {
767
                    if ($id > 0 && $column !== 'id') {
768
                        $q->orWhere($column, $id);
769
                    }
770
771
                }
772
            });
773
        }
774
775
        if (! empty($name)) {
776
            // If $name is set it is a fallback search, add available SxxExx/airdate info to the query
777
            if (! empty($series) && (int) $series < 1900) {
778
                $name .= sprintf(' S%s', str_pad($series, 2, '0', STR_PAD_LEFT));
779
                if (! empty($episode) && ! str_contains($episode, '/')) {
780
                    $name .= sprintf('E%s', str_pad($episode, 2, '0', STR_PAD_LEFT));
781
                }
782
                // If season is not empty but episode is, add a wildcard to the search
783
                if (empty($episode)) {
784
                    $name .= '*';
785
                }
786
            } elseif (! empty($airDate)) {
787
                $name .= sprintf(' %s', str_replace(['/', '-', '.', '_'], ' ', $airDate));
788
            }
789
790
            if (config('nntmux.elasticsearch_enabled') === true) {
791
                $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit);
792
            } else {
793
                $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']);
794
                if (! empty($searchResult)) {
795
                    $searchResult = Arr::wrap(Arr::get($searchResult, 'id'));
796
                }
797
            }
798
799
            if (count($searchResult) === 0) {
800
                return collect();
801
            }
802
803
            $query->whereIn('id', $searchResult);
804
        }
805
806
        if ($maxAge > 0) {
807
            $query->where('postdate', '>', now()->subDays($maxAge));
808
        }
809
810
        if (! empty($excludedCategories)) {
811
            $query->whereNotIn('categories_id', $excludedCategories);
812
        }
813
814
        if ($cat !== [-1]) {
815
            $query->whereIn('categories_id', $cat);
816
        }
817
818
        if ($minSize > 0) {
819
            $query->where('size', '>=', $minSize);
820
        }
821
822
        $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

822
        $query->orderBy(/** @scrutinizer ignore-type */ 'postdate', 'desc')
Loading history...
823
            ->offset($offset)
824
            ->limit($limit);
825
826
        $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

826
        $cacheKey = md5(/** @scrutinizer ignore-type */ $query->toRawSql());
Loading history...
827
        $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium'));
828
829
        $releases = Cache::get($cacheKey);
830
        if ($releases !== null) {
831
            return $releases;
832
        }
833
834
        $releases = $query->get();
835
836
        if ($releases->isNotEmpty()) {
837
            $releases[0]->_totalrows = $query->count();
838
        }
839
840
        Cache::put($cacheKey, $releases, $cacheTTL);
841
842
        return $releases;
843
    }
844
845
    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
846
    {
847
        $query = self::query()
848
            ->with(['movieinfo', 'group', 'category', 'category.parent', 'nfo'])
849
            ->whereBetween('categories_id', [Category::MOVIE_ROOT, Category::MOVIE_OTHER])
850
            ->where('nzbstatus', NZB::NZB_ADDED)
851
            ->where('passwordstatus', $this->showPasswords());
852
853
        if (! empty($name)) {
854
            if (config('nntmux.elasticsearch_enabled') === true) {
855
                $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit);
856
            } else {
857
                $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']);
858
                if (! empty($searchResult)) {
859
                    $searchResult = Arr::wrap(Arr::get($searchResult, 'id'));
860
                }
861
            }
862
863
            if (count($searchResult) === 0) {
864
                return collect();
865
            }
866
867
            $query->whereIn('id', $searchResult);
868
        }
869
870
        if ($imDbId !== -1 && is_numeric($imDbId)) {
871
            $query->whereHas('movieinfo', function ($q) use ($imDbId) {
872
                $q->where('imdbid', $imDbId);
873
            });
874
        }
875
876
        if ($tmDbId !== -1 && is_numeric($tmDbId)) {
877
            $query->whereHas('movieinfo', function ($q) use ($tmDbId) {
878
                $q->where('tmdbid', $tmDbId);
879
            });
880
        }
881
882
        if ($traktId !== -1 && is_numeric($traktId)) {
883
            $query->whereHas('movieinfo', function ($q) use ($traktId) {
884
                $q->where('traktid', $traktId);
885
            });
886
        }
887
888
        if (! empty($excludedCategories)) {
889
            $query->whereNotIn('categories_id', $excludedCategories);
890
        }
891
892
        if ($cat !== [-1]) {
893
            $query->whereIn('categories_id', $cat);
894
        }
895
896
        if ($maxAge > 0) {
897
            $query->where('postdate', '>', now()->subDays($maxAge));
898
        }
899
900
        if ($minSize > 0) {
901
            $query->where('size', '>=', $minSize);
902
        }
903
904
        $totalRows = $query->count();
905
906
        $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

906
        $query->orderBy(/** @scrutinizer ignore-type */ 'postdate', 'desc')
Loading history...
907
            ->offset($offset)
908
            ->limit($limit);
909
910
        $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

910
        $cacheKey = md5(/** @scrutinizer ignore-type */ $query->toRawSql());
Loading history...
911
        $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium'));
912
913
        $releases = Cache::get($cacheKey);
914
        if ($releases !== null) {
915
            return $releases;
916
        }
917
918
        $releases = $query->get();
919
920
        if ($releases->isNotEmpty()) {
921
            $releases[0]->_totalrows = $totalRows;
922
        }
923
924
        Cache::put($cacheKey, $releases, $cacheTTL);
925
926
        return $releases;
927
    }
928
929
    public function searchSimilar($currentID, $name, array $excludedCats = []): bool|array
930
    {
931
        // Get the category for the parent of this release.
932
        $ret = false;
933
        $currRow = self::getCatByRelId($currentID);
934
        if ($currRow !== null) {
935
            $catRow = Category::find($currRow['categories_id']);
936
            $parentCat = $catRow !== null ? $catRow['root_categories_id'] : null;
937
938
            if ($parentCat === null) {
939
                return $ret;
940
            }
941
942
            $results = $this->search(['searchname' => getSimilarName($name)], -1, '', '', -1, -1, 0, config('nntmux.items_per_page'), '', -1, $excludedCats, 'basic', [$parentCat]);
943
            if (! $results) {
944
                return $ret;
945
            }
946
947
            $ret = [];
948
            foreach ($results as $res) {
949
                if ($res['id'] !== $currentID && $res['categoryparentid'] === $parentCat) {
950
                    $ret[] = $res;
951
                }
952
            }
953
        }
954
955
        return $ret;
956
    }
957
958
    /**
959
     * Get count of releases for pager.
960
     *
961
     * @param  string  $query  The query to get the count from.
962
     */
963
    private function getPagerCount(string $query): int
964
    {
965
        $queryBuilder = DB::table(DB::raw('('.preg_replace(
966
            '/SELECT.+?FROM\s+releases/is',
967
            'SELECT r.id FROM releases',
968
            $query
969
        ).' LIMIT '.(int) config('nntmux.max_pager_results').') as z'))
970
            ->selectRaw('COUNT(z.id) as count');
971
972
        $sql = $queryBuilder->toSql();
973
        $count = Cache::get(md5($sql));
974
975
        if ($count !== null) {
976
            return $count;
977
        }
978
979
        $result = $queryBuilder->first();
980
        $count = $result->count ?? 0;
981
982
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_short'));
983
        Cache::put(md5($sql), $count, $expiresAt);
984
985
        return $count;
986
    }
987
}
988