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

Releases::updateMulti()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 16
rs 9.9332
c 0
b 0
f 0
cc 4
nc 3
nop 7
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