ReleaseBrowseService::getBrowseRange()   B
last analyzed

Complexity

Conditions 8
Paths 3

Size

Total Lines 62
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 53
c 1
b 0
f 0
dl 0
loc 62
rs 7.781
cc 8
nc 3
nop 9

How to fix   Long Method    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 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