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

ReleaseBrowseService::getPagerCount()   B

Complexity

Conditions 8
Paths 21

Size

Total Lines 43
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 24
c 2
b 0
f 0
dl 0
loc 43
rs 8.4444
cc 8
nc 21
nop 1
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