Passed
Push — master ( 85b490...1de0a7 )
by Darko
10:33
created

ReleaseBrowseService::getBrowseRange()   B

Complexity

Conditions 10
Paths 96

Size

Total Lines 66
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 51
c 2
b 0
f 0
dl 0
loc 66
rs 7.2024
cc 10
nc 96
nop 9

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 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
     * Optimized query - only fetches fields actually used in views.
30
     *
31
     * @return Collection|mixed
32
     */
33
    public function getBrowseRange($page, $cat, $start, $num, $orderBy, int $maxAge = -1, array $excludedCats = [], int|string $groupName = -1, int $minSize = 0): mixed
34
    {
35
        $cacheVersion = $this->getCacheVersion();
36
        $page = max(1, $page);
0 ignored issues
show
Unused Code introduced by
The assignment to $page is dead and can be removed.
Loading history...
37
        $start = max(0, $start);
38
39
        $orderBy = $this->getBrowseOrder($orderBy);
40
41
        // Build WHERE conditions once
42
        $categorySearch = Category::getCategorySearch($cat);
43
        $ageCondition = $maxAge > 0 ? ' AND r.postdate > NOW() - INTERVAL '.$maxAge.' DAY ' : '';
44
        $excludeCondition = \count($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : '';
45
        $sizeCondition = $minSize > 0 ? sprintf(' AND r.size >= %d', $minSize) : '';
46
        $limitClause = ' LIMIT '.$num.($start > 0 ? ' OFFSET '.$start : '');
47
        $needsGroupJoin = (int) $groupName !== -1;
48
        $groupCondition = $needsGroupJoin ? sprintf(' AND g.name = %s ', escapeString($groupName)) : '';
49
50
        // Optimized query: fetch only required fields, minimize JOINs
51
        // Uses STRAIGHT_JOIN for categories (small tables), LEFT JOIN for optional data
52
        $qry = sprintf(
53
            "SELECT r.id, r.searchname, r.guid, r.postdate, r.categories_id, r.size, r.totalpart,
54
                r.fromname, r.grabs, r.comments, r.adddate, r.videos_id, r.haspreview,
55
                r.jpgstatus, r.nfostatus,
56
                CONCAT(cp.title, ' > ', c.title) AS category_name,
57
                %s AS group_name,
58
                m.imdbid,
59
                (SELECT COUNT(*) FROM dnzb_failures df WHERE df.release_id = r.id) AS failed,
60
                EXISTS(SELECT 1 FROM video_data vd WHERE vd.releases_id = r.id) AS reid
61
            FROM releases r
62
            %s
63
            STRAIGHT_JOIN categories c ON c.id = r.categories_id
64
            STRAIGHT_JOIN root_categories cp ON cp.id = c.root_categories_id
65
            LEFT JOIN movieinfo m ON m.id = r.movieinfo_id
66
            WHERE r.passwordstatus %s
67
            %s %s %s %s %s
68
            ORDER BY r.%s %s
69
            %s",
70
            $needsGroupJoin ? 'g.name' : '(SELECT name FROM usenet_groups WHERE id = r.groups_id)',
71
            $needsGroupJoin ? 'INNER JOIN usenet_groups g ON g.id = r.groups_id' : '',
72
            $this->showPasswords(),
73
            $categorySearch,
74
            $ageCondition,
75
            $excludeCondition,
76
            $sizeCondition,
77
            $groupCondition,
78
            $orderBy[0],
79
            $orderBy[1],
80
            $limitClause
81
        );
82
83
        $cacheKey = 'browse_'.md5($cacheVersion.$qry);
84
        $releases = Cache::get($cacheKey);
85
        if ($releases !== null) {
86
            return $releases;
87
        }
88
89
        $sql = DB::select($qry);
90
        if (\count($sql) > 0) {
91
            $possibleRows = $this->getBrowseCount($cat, $maxAge, $excludedCats, $groupName);
92
            $sql[0]->_totalcount = $sql[0]->_totalrows = $possibleRows;
93
        }
94
95
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium'));
96
        Cache::put($cacheKey, $sql, $expiresAt);
97
98
        return $sql;
99
    }
100
101
    /**
102
     * Used for pager on browse page.
103
     * Optimized to avoid unnecessary JOINs and use faster counting.
104
     */
105
    public function getBrowseCount(array $cat, int $maxAge = -1, array $excludedCats = [], int|string $groupName = ''): int
106
    {
107
        $needsGroupJoin = $groupName !== '' && $groupName !== -1;
108
109
        return $this->getPagerCount(sprintf(
110
            'SELECT COUNT(*) AS count
111
                FROM releases r
112
                %s
113
                WHERE r.passwordstatus %s
114
                %s
115
                %s %s %s',
116
            $needsGroupJoin ? 'INNER JOIN usenet_groups g ON g.id = r.groups_id' : '',
117
            $this->showPasswords(),
118
            $needsGroupJoin ? sprintf(' AND g.name = %s', escapeString($groupName)) : '',
119
            Category::getCategorySearch($cat),
120
            $maxAge > 0 ? ' AND r.postdate > NOW() - INTERVAL '.$maxAge.' DAY ' : '',
121
            \count($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''
122
        ));
123
    }
124
125
    /**
126
     * Get the passworded releases clause.
127
     */
128
    public function showPasswords(): string
129
    {
130
        $show = (int) Settings::settingValue('showpasswordedrelease');
131
        $setting = $show ?? 0;
132
133
        return match ($setting) {
134
            1 => '<= '.self::PASSWD_RAR,
135
            default => '= '.self::PASSWD_NONE,
136
        };
137
    }
138
139
    /**
140
     * Use to order releases on site.
141
     */
142
    public function getBrowseOrder(array|string $orderBy): array
143
    {
144
        $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

144
        $orderArr = explode('_', /** @scrutinizer ignore-type */ ($orderBy === '' ? 'posted_desc' : $orderBy));
Loading history...
145
        $orderField = match ($orderArr[0]) {
146
            'cat' => 'categories_id',
147
            'name' => 'searchname',
148
            'size' => 'size',
149
            'files' => 'totalpart',
150
            'stats' => 'grabs',
151
            default => 'postdate',
152
        };
153
154
        return [$orderField, isset($orderArr[1]) && preg_match('/^(asc|desc)$/i', $orderArr[1]) ? $orderArr[1] : 'desc'];
155
    }
156
157
    /**
158
     * Return ordering types usable on site.
159
     *
160
     * @return string[]
161
     */
162
    public function getBrowseOrdering(): array
163
    {
164
        return [
165
            'name_asc',
166
            'name_desc',
167
            'cat_asc',
168
            'cat_desc',
169
            'posted_asc',
170
            'posted_desc',
171
            'size_asc',
172
            'size_desc',
173
            'files_asc',
174
            'files_desc',
175
            'stats_asc',
176
            'stats_desc',
177
        ];
178
    }
179
180
    /**
181
     * @return \Illuminate\Database\Eloquent\Collection|mixed
182
     */
183
    public function getShowsRange($userShows, $offset, $limit, $orderBy, int $maxAge = -1, array $excludedCats = [])
184
    {
185
        $cacheVersion = $this->getCacheVersion();
186
        $orderBy = $this->getBrowseOrder($orderBy);
187
        $sql = sprintf(
188
            "SELECT r.id, r.searchname, r.guid, r.postdate, r.categories_id, r.size, r.totalpart,
189
                r.fromname, r.grabs, r.comments, r.adddate, r.videos_id, r.haspreview, r.jpgstatus,
190
                CONCAT(cp.title, ' > ', c.title) AS category_name
191
            FROM releases r
192
            STRAIGHT_JOIN categories c ON c.id = r.categories_id
193
            STRAIGHT_JOIN root_categories cp ON cp.id = c.root_categories_id
194
            WHERE %s %s
195
                AND r.categories_id BETWEEN %d AND %d
196
                AND r.passwordstatus %s
197
                %s
198
            ORDER BY r.%s %s %s",
199
            $this->uSQL($userShows, 'videos_id'),
200
            (! empty($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''),
201
            Category::TV_ROOT,
202
            Category::TV_OTHER,
203
            $this->showPasswords(),
204
            ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : ''),
205
            $orderBy[0],
206
            $orderBy[1],
207
            ($offset === false ? '' : (' LIMIT '.$limit.' OFFSET '.$offset))
208
        );
209
        $cacheKey = 'shows_'.md5($cacheVersion.$sql);
210
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_long'));
211
        $result = Cache::get($cacheKey);
212
        if ($result !== null) {
213
            return $result;
214
        }
215
        $result = Release::fromQuery($sql);
216
        Cache::put($cacheKey, $result, $expiresAt);
217
218
        return $result;
219
    }
220
221
    public function getShowsCount($userShows, int $maxAge = -1, array $excludedCats = []): int
222
    {
223
        return $this->getPagerCount(
224
            sprintf(
225
                'SELECT COUNT(*) AS count
226
				FROM releases r
227
				WHERE %s %s
228
				AND r.categories_id BETWEEN %d AND %d
229
				AND r.passwordstatus %s
230
				%s',
231
                $this->uSQL($userShows, 'videos_id'),
232
                (\count($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''),
233
                Category::TV_ROOT,
234
                Category::TV_OTHER,
235
                $this->showPasswords(),
236
                ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : '')
237
            )
238
        );
239
    }
240
241
    /**
242
     * Creates part of a query for some functions.
243
     */
244
    public function uSQL(Collection|array|\Illuminate\Support\Collection $userQuery, string $type): string
245
    {
246
        $sql = '(1=2 ';
247
        foreach ($userQuery as $query) {
248
            $sql .= sprintf('OR (r.%s = %d', $type, $query->$type);
249
            if (! empty($query->categories)) {
250
                $catsArr = explode('|', $query->categories);
251
                if (\count($catsArr) > 1) {
252
                    $sql .= sprintf(' AND r.categories_id IN (%s)', implode(',', $catsArr));
253
                } else {
254
                    $sql .= sprintf(' AND r.categories_id = %d', $catsArr[0]);
255
                }
256
            }
257
            $sql .= ') ';
258
        }
259
        $sql .= ') ';
260
261
        return $sql;
262
    }
263
264
    public static function bumpCacheVersion(): void
265
    {
266
        $current = Cache::get(self::CACHE_VERSION_KEY, 1);
267
        Cache::forever(self::CACHE_VERSION_KEY, $current + 1);
268
    }
269
270
    private function getCacheVersion(): int
271
    {
272
        return Cache::get(self::CACHE_VERSION_KEY, 1);
273
    }
274
275
    /**
276
     * Get the count of releases for pager.
277
     * Optimized: expects COUNT(*) queries directly for best performance.
278
     *
279
     * @param  string  $query  The COUNT query to execute.
280
     */
281
    private function getPagerCount(string $query): int
282
    {
283
        $maxResults = (int) config('nntmux.max_pager_results');
284
        $cacheExpiry = (int) config('nntmux.cache_expiry_short', 5);
285
286
        $cacheKey = 'pager_count_'.md5($query);
287
288
        $count = Cache::get($cacheKey);
289
        if ($count !== null) {
290
            return (int) $count;
291
        }
292
293
        try {
294
            $result = DB::select($query);
295
            $count = 0;
296
297
            if (isset($result[0])) {
298
                // Handle the count result
299
                $count = $result[0]->count ?? 0;
300
                if ($count === 0) {
301
                    // Fallback: get first property value
302
                    foreach ($result[0] as $value) {
303
                        $count = (int) $value;
304
                        break;
305
                    }
306
                }
307
            }
308
309
            // Cap at max results if configured
310
            if ($maxResults > 0 && $count > $maxResults) {
311
                $count = $maxResults;
312
            }
313
314
            Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
315
316
            return (int) $count;
317
        } catch (\Exception $e) {
318
            \Illuminate\Support\Facades\Log::error('getPagerCount failed', [
319
                'query' => $query,
320
                'error' => $e->getMessage(),
321
            ]);
322
323
            return 0;
324
        }
325
    }
326
}
327
328