Files
newznab-tmux-NNTmux/app/Services/Releases/ReleaseBrowseService.php
T

412 lines
15 KiB
PHP

<?php
namespace App\Services\Releases;
use App\Models\Category;
use App\Models\Release;
use App\Models\Settings;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
/**
* Service for browsing and ordering releases on the frontend.
*/
class ReleaseBrowseService
{
private const CACHE_VERSION_KEY = 'releases:cache_version';
// RAR/ZIP Password indicator.
public const PASSWD_NONE = 0; // No password.
public const PASSWD_RAR = 1; // Definitely passworded.
public function __construct()
{
}
/**
* Used for Browse results.
*
* @return Collection|mixed
*/
public function getBrowseRange($page, $cat, $start, $num, $orderBy, int $maxAge = -1, array $excludedCats = [], int|string $groupName = -1, int $minSize = 0): mixed
{
$cacheVersion = $this->getCacheVersion();
$page = max(1, $page);
$start = max(0, $start);
$orderBy = $this->getBrowseOrder($orderBy);
$qry = sprintf(
"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,
CONCAT(cp.title, ' > ', c.title) AS category_name,
CONCAT(cp.id, ',', c.id) AS category_ids,
df.failed AS failed,
rn.releases_id AS nfoid,
re.releases_id AS reid,
v.tvdb, v.trakt, v.tvrage, v.tvmaze, v.imdb, v.tmdb,
m.imdbid, m.tmdbid, m.traktid,
tve.title, tve.firstaired
FROM
(
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
FROM releases r
LEFT JOIN usenet_groups g ON g.id = r.groups_id
WHERE r.passwordstatus %s
%s %s %s %s %s
ORDER BY %s %s %s
) r
LEFT JOIN categories c ON c.id = r.categories_id
LEFT JOIN root_categories cp ON cp.id = c.root_categories_id
LEFT OUTER JOIN videos v ON r.videos_id = v.id
LEFT OUTER JOIN tv_episodes tve ON r.tv_episodes_id = tve.id
LEFT OUTER JOIN movieinfo m ON m.id = r.movieinfo_id
LEFT OUTER JOIN video_data re ON re.releases_id = r.id
LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id
LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id
GROUP BY r.id
ORDER BY %7\$s %8\$s",
$this->showPasswords(),
Category::getCategorySearch($cat),
($maxAge > 0 ? (' AND postdate > NOW() - INTERVAL '.$maxAge.' DAY ') : ''),
(\count($excludedCats) ? (' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')') : ''),
((int) $groupName !== -1 ? sprintf(' AND g.name = %s ', escapeString($groupName)) : ''),
($minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : ''),
$orderBy[0],
$orderBy[1],
($start === 0 ? ' LIMIT '.$num : ' LIMIT '.$num.' OFFSET '.$start)
);
$cacheKey = md5($cacheVersion.$qry.$page);
$releases = Cache::get($cacheKey);
if ($releases !== null) {
return $releases;
}
$sql = DB::select($qry);
if (\count($sql) > 0) {
$possibleRows = $this->getBrowseCount($cat, $maxAge, $excludedCats, $groupName);
$sql[0]->_totalcount = $sql[0]->_totalrows = $possibleRows;
}
$expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium'));
Cache::put($cacheKey, $sql, $expiresAt);
return $sql;
}
/**
* Used for pager on browse page.
*/
public function getBrowseCount(array $cat, int $maxAge = -1, array $excludedCats = [], int|string $groupName = ''): int
{
return $this->getPagerCount(sprintf(
'SELECT COUNT(r.id) AS count
FROM releases r
%s
WHERE r.passwordstatus %s
%s
%s %s %s ',
($groupName !== -1 ? 'LEFT JOIN usenet_groups g ON g.id = r.groups_id' : ''),
$this->showPasswords(),
($groupName !== -1 ? sprintf(' AND g.name = %s', escapeString($groupName)) : ''),
Category::getCategorySearch($cat),
($maxAge > 0 ? (' AND r.postdate > NOW() - INTERVAL '.$maxAge.' DAY ') : ''),
(\count($excludedCats) ? (' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')') : '')
));
}
/**
* Get the passworded releases clause.
*/
public function showPasswords(): string
{
$show = (int) Settings::settingValue('showpasswordedrelease');
$setting = $show ?? 0;
return match ($setting) {
1 => '<= '.self::PASSWD_RAR,
default => '= '.self::PASSWD_NONE,
};
}
/**
* Use to order releases on site.
*/
public function getBrowseOrder(array|string $orderBy): array
{
$orderArr = explode('_', ($orderBy === '' ? 'posted_desc' : $orderBy));
$orderField = match ($orderArr[0]) {
'cat' => 'categories_id',
'name' => 'searchname',
'size' => 'size',
'files' => 'totalpart',
'stats' => 'grabs',
default => 'postdate',
};
return [$orderField, isset($orderArr[1]) && preg_match('/^(asc|desc)$/i', $orderArr[1]) ? $orderArr[1] : 'desc'];
}
/**
* Return ordering types usable on site.
*
* @return string[]
*/
public function getBrowseOrdering(): array
{
return [
'name_asc',
'name_desc',
'cat_asc',
'cat_desc',
'posted_asc',
'posted_desc',
'size_asc',
'size_desc',
'files_asc',
'files_desc',
'stats_asc',
'stats_desc',
];
}
/**
* @return \Illuminate\Cache\|\Illuminate\Database\Eloquent\Collection|mixed
*/
public function getShowsRange($userShows, $offset, $limit, $orderBy, int $maxAge = -1, array $excludedCats = [])
{
$orderBy = $this->getBrowseOrder($orderBy);
$sql = sprintf(
"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,
CONCAT(cp.title, '->', c.title) AS category_name
FROM releases r
LEFT JOIN categories c ON c.id = r.categories_id
LEFT JOIN root_categories cp ON cp.id = c.root_categories_id
WHERE %s %s
AND r.categories_id BETWEEN %d AND %d
AND r.passwordstatus %s
%s
GROUP BY r.id
ORDER BY %s %s %s",
$this->uSQL($userShows, 'videos_id'),
(! empty($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''),
Category::TV_ROOT,
Category::TV_OTHER,
$this->showPasswords(),
($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : ''),
$orderBy[0],
$orderBy[1],
($offset === false ? '' : (' LIMIT '.$limit.' OFFSET '.$offset))
);
$expiresAt = now()->addMinutes(config('nntmux.cache_expiry_long'));
$result = Cache::get(md5($sql));
if ($result !== null) {
return $result;
}
$result = Release::fromQuery($sql);
Cache::put(md5($sql), $result, $expiresAt);
return $result;
}
public function getShowsCount($userShows, int $maxAge = -1, array $excludedCats = []): int
{
return $this->getPagerCount(
sprintf(
'SELECT r.id
FROM releases r
WHERE %s %s
AND r.categories_id BETWEEN %d AND %d
AND r.passwordstatus %s
%s',
$this->uSQL($userShows, 'videos_id'),
(\count($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''),
Category::TV_ROOT,
Category::TV_OTHER,
$this->showPasswords(),
($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : '')
)
);
}
/**
* Creates part of a query for some functions.
*/
public function uSQL(Collection|array $userQuery, string $type): string
{
$sql = '(1=2 ';
foreach ($userQuery as $query) {
$sql .= sprintf('OR (r.%s = %d', $type, $query->$type);
if (! empty($query->categories)) {
$catsArr = explode('|', $query->categories);
if (\count($catsArr) > 1) {
$sql .= sprintf(' AND r.categories_id IN (%s)', implode(',', $catsArr));
} else {
$sql .= sprintf(' AND r.categories_id = %d', $catsArr[0]);
}
}
$sql .= ') ';
}
$sql .= ') ';
return $sql;
}
public static function bumpCacheVersion(): void
{
$current = Cache::get(self::CACHE_VERSION_KEY, 1);
Cache::forever(self::CACHE_VERSION_KEY, $current + 1);
}
private function getCacheVersion(): int
{
return Cache::get(self::CACHE_VERSION_KEY, 1);
}
/**
* Get the count of releases for pager.
*
* @param string $query The query to get the count from.
*/
private function getPagerCount(string $query): int
{
$maxResults = (int) config('nntmux.max_pager_results');
$cacheExpiry = config('nntmux.cache_expiry_short');
// Generate cache key from original query
$cacheKey = 'pager_count_'.md5($query);
// Check cache first
$count = Cache::get($cacheKey);
if ($count !== null) {
return (int) $count;
}
// Check if this is already a COUNT query
if (preg_match('/SELECT\s+COUNT\s*\(/is', $query)) {
// It's already a COUNT query, just execute it
try {
$result = DB::select($query);
if (isset($result[0])) {
// Handle different possible column names
$count = $result[0]->count ?? $result[0]->total ?? 0;
// Check for COUNT(*) result without alias
if ($count === 0) {
foreach ($result[0] as $value) {
$count = (int) $value;
break;
}
}
} else {
$count = 0;
}
// Cap the count at max results if applicable
if ($maxResults > 0 && $count > $maxResults) {
$count = $maxResults;
}
// Cache the result
Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
return $count;
} catch (\Exception $e) {
return 0;
}
}
// For regular SELECT queries, optimize for counting
$countQuery = $query;
// Remove ORDER BY clause (not needed for COUNT)
$countQuery = preg_replace('/ORDER\s+BY\s+[^)]+$/is', '', $countQuery);
// Remove GROUP BY if it's only grouping by r.id
$countQuery = preg_replace('/GROUP\s+BY\s+r\.id\s*$/is', '', $countQuery);
// Check if query has DISTINCT in SELECT
$hasDistinct = preg_match('/SELECT\s+DISTINCT/is', $countQuery);
// Replace SELECT clause with COUNT
if ($hasDistinct || preg_match('/GROUP\s+BY/is', $countQuery)) {
// For queries with DISTINCT or GROUP BY, count distinct r.id
$countQuery = preg_replace(
'/SELECT\s+.+?\s+FROM/is',
'SELECT COUNT(DISTINCT r.id) as count FROM',
$countQuery
);
} else {
// For simple queries, use COUNT(*)
$countQuery = preg_replace(
'/SELECT\s+.+?\s+FROM/is',
'SELECT COUNT(*) as count FROM',
$countQuery
);
}
// Remove LIMIT/OFFSET from the count query
$countQuery = preg_replace('/LIMIT\s+\d+(\s+OFFSET\s+\d+)?$/is', '', $countQuery);
try {
// If max results is set and query might return too many results
if ($maxResults > 0) {
// First check if count would exceed max
$testQuery = sprintf('SELECT 1 FROM (%s) as test LIMIT %d',
preg_replace('/SELECT\s+COUNT.+?\s+FROM/is', 'SELECT 1 FROM', $countQuery),
$maxResults + 1
);
$testResult = DB::select($testQuery);
if (count($testResult) > $maxResults) {
Cache::put($cacheKey, $maxResults, now()->addMinutes($cacheExpiry));
return $maxResults;
}
}
// Execute the count query
$result = DB::select($countQuery);
$count = isset($result[0]) ? (int) $result[0]->count : 0;
// Cache the result
Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
return $count;
} catch (\Exception $e) {
// If optimization fails, try a simpler approach
try {
// Extract the core table and WHERE conditions
if (preg_match('/FROM\s+releases\s+r\s+(.+?)(?:ORDER\s+BY|LIMIT|$)/is', $query, $matches)) {
$conditions = $matches[1];
// Remove JOINs but keep WHERE
$conditions = preg_replace('/(?:LEFT\s+|INNER\s+)?(?:OUTER\s+)?JOIN\s+.+?(?=WHERE|LEFT|INNER|JOIN|$)/is', '', $conditions);
$fallbackQuery = sprintf('SELECT COUNT(*) as count FROM releases r %s', trim($conditions));
if ($maxResults > 0) {
$fallbackQuery = sprintf('SELECT COUNT(*) as count FROM (SELECT 1 FROM releases r %s LIMIT %d) as limited',
trim($conditions),
$maxResults
);
}
$result = DB::select($fallbackQuery);
$count = isset($result[0]) ? (int) $result[0]->count : 0;
Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
return $count;
}
} catch (\Exception $fallbackException) {
// Log the error for debugging
\Illuminate\Support\Facades\Log::error('getPagerCount failed', [
'query' => $query,
'error' => $fallbackException->getMessage(),
]);
}
return 0;
}
}
}