Passed
Push — master ( 710bcf...85b490 )
by Darko
10:33
created

ReleaseBrowseService::showPasswords()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 8
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace App\Services\Releases;
4
5
use App\Models\Category;
6
use App\Models\Release;
7
use App\Models\Settings;
8
use Illuminate\Database\Eloquent\Collection;
9
use Illuminate\Support\Facades\Cache;
10
use Illuminate\Support\Facades\DB;
11
12
/**
13
 * Service for browsing and ordering releases on the frontend.
14
 */
15
class ReleaseBrowseService
16
{
17
    private const CACHE_VERSION_KEY = 'releases:cache_version';
18
19
    // RAR/ZIP Password indicator.
20
    public const PASSWD_NONE = 0; // No password.
21
    public const PASSWD_RAR = 1; // Definitely passworded.
22
23
    public function __construct()
24
    {
25
    }
26
27
    /**
28
     * Used for Browse results.
29
     *
30
     * @return Collection|mixed
31
     */
32
    public function getBrowseRange($page, $cat, $start, $num, $orderBy, int $maxAge = -1, array $excludedCats = [], int|string $groupName = -1, int $minSize = 0): mixed
33
    {
34
        $cacheVersion = $this->getCacheVersion();
35
        $page = max(1, $page);
36
        $start = max(0, $start);
37
38
        $orderBy = $this->getBrowseOrder($orderBy);
39
40
        $qry = sprintf(
41
            "SELECT r.id, r.searchname, r.groups_id, r.guid, r.postdate, 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, r.nfostatus, cp.title AS parent_category, c.title AS sub_category, r.group_name,
42
				CONCAT(cp.title, ' > ', c.title) AS category_name,
43
				CONCAT(cp.id, ',', c.id) AS category_ids,
44
				df.failed AS failed,
45
				rn.releases_id AS nfoid,
46
				re.releases_id AS reid,
47
				v.tvdb, v.trakt, v.tvrage, v.tvmaze, v.imdb, v.tmdb,
48
				m.imdbid, m.tmdbid, m.traktid,
49
				tve.title, tve.firstaired
50
			FROM
51
			(
52
				SELECT r.id, 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, r.nfostatus, g.name AS group_name, r.movieinfo_id
53
				FROM releases r
54
				LEFT JOIN usenet_groups g ON g.id = r.groups_id
55
				WHERE r.passwordstatus %s
56
				%s %s %s %s %s
57
				ORDER BY %s %s %s
58
			) r
59
			LEFT JOIN categories c ON c.id = r.categories_id
60
			LEFT JOIN root_categories cp ON cp.id = c.root_categories_id
61
			LEFT OUTER JOIN videos v ON r.videos_id = v.id
62
			LEFT OUTER JOIN tv_episodes tve ON r.tv_episodes_id = tve.id
63
			LEFT OUTER JOIN movieinfo m ON m.id = r.movieinfo_id
64
			LEFT OUTER JOIN video_data re ON re.releases_id = r.id
65
			LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id
66
			LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id
67
			GROUP BY r.id
68
			ORDER BY %7\$s %8\$s",
69
            $this->showPasswords(),
70
            Category::getCategorySearch($cat),
71
            ($maxAge > 0 ? (' AND postdate > NOW() - INTERVAL '.$maxAge.' DAY ') : ''),
72
            (\count($excludedCats) ? (' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')') : ''),
73
            ((int) $groupName !== -1 ? sprintf(' AND g.name = %s ', escapeString($groupName)) : ''),
74
            ($minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : ''),
75
            $orderBy[0],
76
            $orderBy[1],
77
            ($start === 0 ? ' LIMIT '.$num : ' LIMIT '.$num.' OFFSET '.$start)
78
        );
79
80
        $cacheKey = md5($cacheVersion.$qry.$page);
81
        $releases = Cache::get($cacheKey);
82
        if ($releases !== null) {
83
            return $releases;
84
        }
85
        $sql = DB::select($qry);
86
        if (\count($sql) > 0) {
87
            $possibleRows = $this->getBrowseCount($cat, $maxAge, $excludedCats, $groupName);
88
            $sql[0]->_totalcount = $sql[0]->_totalrows = $possibleRows;
89
        }
90
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium'));
91
        Cache::put($cacheKey, $sql, $expiresAt);
92
93
        return $sql;
94
    }
95
96
    /**
97
     * Used for pager on browse page.
98
     */
99
    public function getBrowseCount(array $cat, int $maxAge = -1, array $excludedCats = [], int|string $groupName = ''): int
100
    {
101
        return $this->getPagerCount(sprintf(
102
            'SELECT COUNT(r.id) AS count
103
				FROM releases r
104
				%s
105
				WHERE r.passwordstatus %s
106
				%s
107
				%s %s %s ',
108
            ($groupName !== -1 ? 'LEFT JOIN usenet_groups g ON g.id = r.groups_id' : ''),
109
            $this->showPasswords(),
110
            ($groupName !== -1 ? sprintf(' AND g.name = %s', escapeString($groupName)) : ''),
111
            Category::getCategorySearch($cat),
112
            ($maxAge > 0 ? (' AND r.postdate > NOW() - INTERVAL '.$maxAge.' DAY ') : ''),
113
            (\count($excludedCats) ? (' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')') : '')
114
        ));
115
    }
116
117
    /**
118
     * Get the passworded releases clause.
119
     */
120
    public function showPasswords(): string
121
    {
122
        $show = (int) Settings::settingValue('showpasswordedrelease');
123
        $setting = $show ?? 0;
124
125
        return match ($setting) {
126
            1 => '<= '.self::PASSWD_RAR,
127
            default => '= '.self::PASSWD_NONE,
128
        };
129
    }
130
131
    /**
132
     * Use to order releases on site.
133
     */
134
    public function getBrowseOrder(array|string $orderBy): array
135
    {
136
        $orderArr = explode('_', ($orderBy === '' ? 'posted_desc' : $orderBy));
0 ignored issues
show
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

136
        $orderArr = explode('_', /** @scrutinizer ignore-type */ ($orderBy === '' ? 'posted_desc' : $orderBy));
Loading history...
introduced by
The condition $orderBy === '' is always false.
Loading history...
137
        $orderField = match ($orderArr[0]) {
138
            'cat' => 'categories_id',
139
            'name' => 'searchname',
140
            'size' => 'size',
141
            'files' => 'totalpart',
142
            'stats' => 'grabs',
143
            default => 'postdate',
144
        };
145
146
        return [$orderField, isset($orderArr[1]) && preg_match('/^(asc|desc)$/i', $orderArr[1]) ? $orderArr[1] : 'desc'];
147
    }
148
149
    /**
150
     * Return ordering types usable on site.
151
     *
152
     * @return string[]
153
     */
154
    public function getBrowseOrdering(): array
155
    {
156
        return [
157
            'name_asc',
158
            'name_desc',
159
            'cat_asc',
160
            'cat_desc',
161
            'posted_asc',
162
            'posted_desc',
163
            'size_asc',
164
            'size_desc',
165
            'files_asc',
166
            'files_desc',
167
            'stats_asc',
168
            'stats_desc',
169
        ];
170
    }
171
172
    /**
173
     * @return \Illuminate\Cache\|\Illuminate\Database\Eloquent\Collection|mixed
174
     */
175
    public function getShowsRange($userShows, $offset, $limit, $orderBy, int $maxAge = -1, array $excludedCats = [])
176
    {
177
        $orderBy = $this->getBrowseOrder($orderBy);
178
        $sql = sprintf(
179
            "SELECT r.id, 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,  cp.title AS parent_category, c.title AS sub_category,
180
					CONCAT(cp.title, '->', c.title) AS category_name
181
				FROM releases r
182
				LEFT JOIN categories c ON c.id = r.categories_id
183
				LEFT JOIN root_categories cp ON cp.id = c.root_categories_id
184
				WHERE %s %s
185
				AND r.categories_id BETWEEN %d AND %d
186
				AND r.passwordstatus %s
187
				%s
188
				GROUP BY r.id
189
				ORDER BY %s %s %s",
190
            $this->uSQL($userShows, 'videos_id'),
191
            (! empty($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''),
192
            Category::TV_ROOT,
193
            Category::TV_OTHER,
194
            $this->showPasswords(),
195
            ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : ''),
196
            $orderBy[0],
197
            $orderBy[1],
198
            ($offset === false ? '' : (' LIMIT '.$limit.' OFFSET '.$offset))
199
        );
200
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_long'));
201
        $result = Cache::get(md5($sql));
202
        if ($result !== null) {
203
            return $result;
204
        }
205
        $result = Release::fromQuery($sql);
206
        Cache::put(md5($sql), $result, $expiresAt);
207
208
        return $result;
209
    }
210
211
    public function getShowsCount($userShows, int $maxAge = -1, array $excludedCats = []): int
212
    {
213
        return $this->getPagerCount(
214
            sprintf(
215
                'SELECT r.id
216
				FROM releases r
217
				WHERE %s %s
218
				AND r.categories_id BETWEEN %d AND %d
219
				AND r.passwordstatus %s
220
				%s',
221
                $this->uSQL($userShows, 'videos_id'),
222
                (\count($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''),
223
                Category::TV_ROOT,
224
                Category::TV_OTHER,
225
                $this->showPasswords(),
226
                ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : '')
227
            )
228
        );
229
    }
230
231
    /**
232
     * Creates part of a query for some functions.
233
     */
234
    public function uSQL(Collection|array $userQuery, string $type): string
235
    {
236
        $sql = '(1=2 ';
237
        foreach ($userQuery as $query) {
238
            $sql .= sprintf('OR (r.%s = %d', $type, $query->$type);
239
            if (! empty($query->categories)) {
240
                $catsArr = explode('|', $query->categories);
241
                if (\count($catsArr) > 1) {
242
                    $sql .= sprintf(' AND r.categories_id IN (%s)', implode(',', $catsArr));
243
                } else {
244
                    $sql .= sprintf(' AND r.categories_id = %d', $catsArr[0]);
245
                }
246
            }
247
            $sql .= ') ';
248
        }
249
        $sql .= ') ';
250
251
        return $sql;
252
    }
253
254
    public static function bumpCacheVersion(): void
255
    {
256
        $current = Cache::get(self::CACHE_VERSION_KEY, 1);
257
        Cache::forever(self::CACHE_VERSION_KEY, $current + 1);
258
    }
259
260
    private function getCacheVersion(): int
261
    {
262
        return Cache::get(self::CACHE_VERSION_KEY, 1);
263
    }
264
265
    /**
266
     * Get the count of releases for pager.
267
     *
268
     * @param  string  $query  The query to get the count from.
269
     */
270
    private function getPagerCount(string $query): int
271
    {
272
        $maxResults = (int) config('nntmux.max_pager_results');
273
        $cacheExpiry = config('nntmux.cache_expiry_short');
274
275
        // Generate cache key from original query
276
        $cacheKey = 'pager_count_'.md5($query);
277
278
        // Check cache first
279
        $count = Cache::get($cacheKey);
280
        if ($count !== null) {
281
            return (int) $count;
282
        }
283
284
        // Check if this is already a COUNT query
285
        if (preg_match('/SELECT\s+COUNT\s*\(/is', $query)) {
286
            // It's already a COUNT query, just execute it
287
            try {
288
                $result = DB::select($query);
289
                if (isset($result[0])) {
290
                    // Handle different possible column names
291
                    $count = $result[0]->count ?? $result[0]->total ?? 0;
292
                    // Check for COUNT(*) result without alias
293
                    if ($count === 0) {
294
                        foreach ($result[0] as $value) {
295
                            $count = (int) $value;
296
                            break;
297
                        }
298
                    }
299
                } else {
300
                    $count = 0;
301
                }
302
303
                // Cap the count at max results if applicable
304
                if ($maxResults > 0 && $count > $maxResults) {
305
                    $count = $maxResults;
306
                }
307
308
                // Cache the result
309
                Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
310
311
                return $count;
312
            } catch (\Exception $e) {
313
                return 0;
314
            }
315
        }
316
317
        // For regular SELECT queries, optimize for counting
318
        $countQuery = $query;
319
320
        // Remove ORDER BY clause (not needed for COUNT)
321
        $countQuery = preg_replace('/ORDER\s+BY\s+[^)]+$/is', '', $countQuery);
322
323
        // Remove GROUP BY if it's only grouping by r.id
324
        $countQuery = preg_replace('/GROUP\s+BY\s+r\.id\s*$/is', '', $countQuery);
325
326
        // Check if query has DISTINCT in SELECT
327
        $hasDistinct = preg_match('/SELECT\s+DISTINCT/is', $countQuery);
328
329
        // Replace SELECT clause with COUNT
330
        if ($hasDistinct || preg_match('/GROUP\s+BY/is', $countQuery)) {
331
            // For queries with DISTINCT or GROUP BY, count distinct r.id
332
            $countQuery = preg_replace(
333
                '/SELECT\s+.+?\s+FROM/is',
334
                'SELECT COUNT(DISTINCT r.id) as count FROM',
335
                $countQuery
336
            );
337
        } else {
338
            // For simple queries, use COUNT(*)
339
            $countQuery = preg_replace(
340
                '/SELECT\s+.+?\s+FROM/is',
341
                'SELECT COUNT(*) as count FROM',
342
                $countQuery
343
            );
344
        }
345
346
        // Remove LIMIT/OFFSET from the count query
347
        $countQuery = preg_replace('/LIMIT\s+\d+(\s+OFFSET\s+\d+)?$/is', '', $countQuery);
348
349
        try {
350
            // If max results is set and query might return too many results
351
            if ($maxResults > 0) {
352
                // First check if count would exceed max
353
                $testQuery = sprintf('SELECT 1 FROM (%s) as test LIMIT %d',
354
                    preg_replace('/SELECT\s+COUNT.+?\s+FROM/is', 'SELECT 1 FROM', $countQuery),
355
                    $maxResults + 1
356
                );
357
358
                $testResult = DB::select($testQuery);
359
                if (count($testResult) > $maxResults) {
360
                    Cache::put($cacheKey, $maxResults, now()->addMinutes($cacheExpiry));
361
362
                    return $maxResults;
363
                }
364
            }
365
366
            // Execute the count query
367
            $result = DB::select($countQuery);
368
            $count = isset($result[0]) ? (int) $result[0]->count : 0;
369
370
            // Cache the result
371
            Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
372
373
            return $count;
374
        } catch (\Exception $e) {
375
            // If optimization fails, try a simpler approach
376
            try {
377
                // Extract the core table and WHERE conditions
378
                if (preg_match('/FROM\s+releases\s+r\s+(.+?)(?:ORDER\s+BY|LIMIT|$)/is', $query, $matches)) {
379
                    $conditions = $matches[1];
380
                    // Remove JOINs but keep WHERE
381
                    $conditions = preg_replace('/(?:LEFT\s+|INNER\s+)?(?:OUTER\s+)?JOIN\s+.+?(?=WHERE|LEFT|INNER|JOIN|$)/is', '', $conditions);
382
383
                    $fallbackQuery = sprintf('SELECT COUNT(*) as count FROM releases r %s', trim($conditions));
384
385
                    if ($maxResults > 0) {
386
                        $fallbackQuery = sprintf('SELECT COUNT(*) as count FROM (SELECT 1 FROM releases r %s LIMIT %d) as limited',
387
                            trim($conditions),
388
                            $maxResults
389
                        );
390
                    }
391
392
                    $result = DB::select($fallbackQuery);
393
                    $count = isset($result[0]) ? (int) $result[0]->count : 0;
394
395
                    Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
396
397
                    return $count;
398
                }
399
            } catch (\Exception $fallbackException) {
400
                // Log the error for debugging
401
                \Illuminate\Support\Facades\Log::error('getPagerCount failed', [
402
                    'query' => $query,
403
                    'error' => $fallbackException->getMessage(),
404
                ]);
405
            }
406
407
            return 0;
408
        }
409
    }
410
}
411
412