| Total Complexity | 45 |
| Total Lines | 309 |
| Duplicated Lines | 0 % |
| Changes | 2 | ||
| Bugs | 0 | Features | 0 |
Complex classes like ReleaseBrowseService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ReleaseBrowseService, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 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() |
||
| 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); |
||
|
|
|||
| 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)); |
||
| 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 |
||
| 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 |
||
| 324 | } |
||
| 325 | } |
||
| 326 | } |
||
| 327 | |||
| 328 |