diff --git a/Blacklight/Releases.php b/Blacklight/Releases.php index 02689d171..3ce591c8c 100644 --- a/Blacklight/Releases.php +++ b/Blacklight/Releases.php @@ -1,668 +1,468 @@ manticoreSearch = new ManticoreSearch; - $this->elasticSearch = new ElasticSearchSiteSearch; - } + use App\Models\Category; + use App\Models\Release; + use App\Models\Settings; + use App\Models\UsenetGroup; + use Elasticsearch; + use Elasticsearch\Common\Exceptions\Missing404Exception; + use Illuminate\Database\Eloquent\Collection; + use Illuminate\Support\Arr; + use Illuminate\Support\Facades\Cache; + use Illuminate\Support\Facades\DB; + use Illuminate\Support\Facades\File; /** - * Used for Browse results. - * - * @return Collection|mixed + * Class Releases. */ - public function getBrowseRange($page, $cat, $start, $num, $orderBy, int $maxAge = -1, array $excludedCats = [], int|string $groupName = -1, int $minSize = 0): mixed + class Releases extends Release { - $page = max(1, $page); - $start = max(0, $start); + // RAR/ZIP Password indicator. + public const PASSWD_NONE = 0; // No password. - $orderBy = $this->getBrowseOrder($orderBy); + public const PASSWD_RAR = 1; // Definitely passworded. - $query = self::query() - ->with(['group', 'category', 'category.parent', 'video', 'video.episode', 'videoData', 'nfo', 'failed']) - ->where('nzbstatus', NZB::NZB_ADDED) - ->where('passwordstatus', $this->showPasswords()); + public int $passwordStatus; - if ($cat) { - $categories = Category::getCategorySearch($cat, null, true); - // If categories is empty, we don't want to return anything. - if ($categories !== null) { - // if we have more than one category, we need to use whereIn - if (count(Arr::wrap($categories)) > 1) { - $query->whereIn('categories_id', $categories); - } else { - $query->where('categories_id', $categories); + private ManticoreSearch $manticoreSearch; + + private ElasticSearchSiteSearch $elasticSearch; + + /** + * @var array Class instances. + * + * @throws \Exception + */ + public function __construct() + { + parent::__construct(); + $this->manticoreSearch = new ManticoreSearch; + $this->elasticSearch = new ElasticSearchSiteSearch; + } + + /** + * 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 + { + $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, cp.title AS parent_category, c.title AS sub_category, g.name as 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, + tve.title, tve.firstaired + FROM releases r + LEFT JOIN usenet_groups g ON g.id = r.groups_id + 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 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 + WHERE r.nzbstatus = %d + AND r.passwordstatus %s + %s %s %s %s %s + GROUP BY r.id + ORDER BY %s %s + LIMIT %d OFFSET %d", + NZB::NZB_ADDED, + $this->showPasswords(), + Category::getCategorySearch($cat), + ($maxAge > 0 ? (' AND r.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], + $num, + $start + ); + + $releases = Cache::get(md5($qry.$page)); + if ($releases !== null) { + return $releases; + } + $sql = self::fromQuery($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(md5($qry.$page), $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.nzbstatus = %d + AND r.passwordstatus %s + %s + %s %s %s ', + ($groupName !== -1 ? 'LEFT JOIN usenet_groups g ON g.id = r.groups_id' : ''), + NZB::NZB_ADDED, + $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).')') : '') + )); + } + + 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 Release[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection + */ + public function getForExport(string $postFrom = '', string $postTo = '', string $groupID = '') + { + $query = self::query() + ->where('r.nzbstatus', NZB::NZB_ADDED) + ->select(['r.searchname', 'r.guid', 'g.name as gname', DB::raw("CONCAT(cp.title,'_',c.title) AS catName")]) + ->from('releases as r') + ->leftJoin('categories as c', 'c.id', '=', 'r.categories_id') + ->leftJoin('root_categories as cp', 'cp.id', '=', 'c.root_categories_id') + ->leftJoin('usenet_groups as g', 'g.id', '=', 'r.groups_id'); + + if ($groupID !== '') { + $query->where('r.groups_id', $groupID); + } + + if ($postFrom !== '') { + $dateParts = explode('/', $postFrom); + if (\count($dateParts) === 3) { + $query->where('r.postdate', '>', $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0].'00:00:00'); } } - } - if ($maxAge > 0) { - $query->where('postdate', '>', now()->subDays($maxAge)); - } - - if (! empty($excludedCats)) { - $query->whereNotIn('categories_id', $excludedCats); - } - - if ($groupName !== -1) { - $query->whereHas('group', function ($q) use ($groupName) { - $q->where('name', $groupName); - }); - } - - if ($minSize > 0) { - $query->where('size', '>=', $minSize); - } - - $query->orderBy($orderBy[0], $orderBy[1]) - ->skip($start) - ->take($num); - $releases = Cache::get(md5($query->toRawSql().$page)); - if ($releases !== null) { - return $releases; - } - - $sql = $query->get(); - if ($sql->isNotEmpty()) { - $possibleRows = $sql->count(); - $sql[0]->_totalcount = $sql[0]->_totalrows = $possibleRows; - } - - $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); - Cache::put(md5($query->toRawSql().$page), $sql, $expiresAt); - - return $sql; - } - - 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 Collection|\Illuminate\Support\Collection|Release[] - */ - public function getForExport(string $postFrom = '', string $postTo = '', string $groupID = ''): Collection|array|\Illuminate\Support\Collection - { - $query = self::query() - ->where('r.nzbstatus', NZB::NZB_ADDED) - ->select(['r.searchname', 'r.guid', 'g.name as gname', DB::raw("CONCAT(cp.title,'_',c.title) AS catName")]) - ->from('releases as r') - ->leftJoin('categories as c', 'c.id', '=', 'r.categories_id') - ->leftJoin('root_categories as cp', 'cp.id', '=', 'c.root_categories_id') - ->leftJoin('usenet_groups as g', 'g.id', '=', 'r.groups_id'); - - if ($groupID !== '') { - $query->where('r.groups_id', $groupID); - } - - if ($postFrom !== '') { - $dateParts = explode('/', $postFrom); - if (\count($dateParts) === 3) { - $query->where('r.postdate', '>', $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0].'00:00:00'); + if ($postTo !== '') { + $dateParts = explode('/', $postTo); + if (\count($dateParts) === 3) { + $query->where('r.postdate', '<', $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0].'23:59:59'); + } } + + return $query->get(); } - if ($postTo !== '') { - $dateParts = explode('/', $postTo); - if (\count($dateParts) === 3) { - $query->where('r.postdate', '<', $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0].'23:59:59'); + /** + * @return mixed|string + */ + public function getEarliestUsenetPostDate(): mixed + { + $row = self::query()->selectRaw("DATE_FORMAT(min(postdate), '%d/%m/%Y') AS postdate")->first(); + + return $row === null ? '01/01/2014' : $row['postdate']; + } + + /** + * @return mixed|string + */ + public function getLatestUsenetPostDate(): mixed + { + $row = self::query()->selectRaw("DATE_FORMAT(max(postdate), '%d/%m/%Y') AS postdate")->first(); + + return $row === null ? '01/01/2014' : $row['postdate']; + } + + public function getReleasedGroupsForSelect(bool $blnIncludeAll = true): array + { + $groups = self::query() + ->selectRaw('DISTINCT g.id, g.name') + ->leftJoin('usenet_groups as g', 'g.id', '=', 'releases.groups_id') + ->get(); + $temp_array = []; + + if ($blnIncludeAll) { + $temp_array[-1] = '--All Groups--'; } + + foreach ($groups as $group) { + $temp_array[$group['id']] = $group['name']; + } + + return $temp_array; } - return $query->get(); - } + /** + * @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.nzbstatus = %d + 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).')' : ''), + NZB::NZB_ADDED, + 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 = self::fromQuery($sql); + Cache::put(md5($sql), $result, $expiresAt); - /** - * @return mixed|string - */ - public function getEarliestUsenetPostDate(): mixed - { - $row = self::query()->selectRaw("DATE_FORMAT(min(postdate), '%d/%m/%Y') AS postdate")->first(); - - return $row === null ? '01/01/2014' : $row['postdate']; - } - - /** - * @return mixed|string - */ - public function getLatestUsenetPostDate(): mixed - { - $row = self::query()->selectRaw("DATE_FORMAT(max(postdate), '%d/%m/%Y') AS postdate")->first(); - - return $row === null ? '01/01/2014' : $row['postdate']; - } - - public function getReleasedGroupsForSelect(bool $blnIncludeAll = true): array - { - $groups = self::query() - ->selectRaw('DISTINCT g.id, g.name') - ->leftJoin('usenet_groups as g', 'g.id', '=', 'releases.groups_id') - ->get(); - $temp_array = []; - - if ($blnIncludeAll) { - $temp_array[-1] = '--All Groups--'; + return $result; } - foreach ($groups as $group) { - $temp_array[$group['id']] = $group['name']; - } - - return $temp_array; - } - - /** - * @return Collection|mixed - */ - public function getShowsRange($userShows, $offset, $limit, $orderBy, int $maxAge = -1, array $excludedCats = []): mixed - { - $orderBy = $this->getBrowseOrder($orderBy); - - $query = self::query() - ->with(['group', 'category', 'category.parent', 'video', 'video.episode']) - ->where('nzbstatus', NZB::NZB_ADDED) - ->where('passwordstatus', $this->showPasswords()) - ->whereBetween('categories_id', [Category::TV_ROOT, Category::TV_OTHER]) - ->when($maxAge > 0, function ($q) use ($maxAge) { - $q->where('postdate', '>', now()->subDays($maxAge)); - }) - ->when(! empty($excludedCats), function ($q) use ($excludedCats) { - $q->whereNotIn('categories_id', $excludedCats); - }) - ->whereRaw($this->uSQL($userShows, 'videos_id')) - ->orderBy($orderBy[0], $orderBy[1]) - ->offset($offset) - ->limit($limit); - - $cacheKey = md5($query->toRawSql()); - $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium')); - - $releases = Cache::get($cacheKey); - if ($releases !== null) { - return $releases; - } - - $releases = $query->get(); - - if ($releases->isNotEmpty()) { - $releases[0]->_totalrows = $query->count(); - } - - Cache::put($cacheKey, $releases, $cacheTTL); - - return $releases; - } - - public function getShowsCount($userShows, int $maxAge = -1, array $excludedCats = []): int - { - return $this->getPagerCount( - sprintf( - 'SELECT r.id + 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.nzbstatus = %d 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).')' : ''), - NZB::NZB_ADDED, - Category::TV_ROOT, - Category::TV_OTHER, - $this->showPasswords(), - ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : '') - ) - ); - } - - /** - * @throws \Exception - */ - public function deleteMultiple(int|array|string $list): void - { - $list = (array) $list; - - $nzb = new NZB; - $releaseImage = new ReleaseImage; - - foreach ($list as $identifier) { - $this->deleteSingle(['g' => $identifier, 'i' => false], $nzb, $releaseImage); - } - } - - /** - * Deletes a single release by GUID, and all the corresponding files. - * - * @param array $identifiers ['g' => Release GUID(mandatory), 'id => ReleaseID(optional, pass - * false)] - * - * @throws \Exception - */ - public function deleteSingle(array $identifiers, NZB $nzb, ReleaseImage $releaseImage): void - { - // Delete NZB from disk. - $nzbPath = $nzb->NZBPath($identifiers['g']); - if (! empty($nzbPath)) { - File::delete($nzbPath); + $this->uSQL($userShows, 'videos_id'), + (\count($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''), + NZB::NZB_ADDED, + Category::TV_ROOT, + Category::TV_OTHER, + $this->showPasswords(), + ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : '') + ) + ); } - // Delete images. - $releaseImage->delete($identifiers['g']); + /** + * @throws \Exception + */ + public function deleteMultiple(int|array|string $list): void + { + $list = (array) $list; - if (config('nntmux.elasticsearch_enabled') === true) { - if ($identifiers['i'] === false) { - $identifiers['i'] = Release::query()->where('guid', $identifiers['g'])->first(['id']); + $nzb = new NZB; + $releaseImage = new ReleaseImage; + + foreach ($list as $identifier) { + $this->deleteSingle(['g' => $identifier, 'i' => false], $nzb, $releaseImage); + } + } + + /** + * Deletes a single release by GUID, and all the corresponding files. + * + * @param array $identifiers ['g' => Release GUID(mandatory), 'id => ReleaseID(optional, pass + * false)] + * + * @throws \Exception + */ + public function deleteSingle(array $identifiers, NZB $nzb, ReleaseImage $releaseImage): void + { + // Delete NZB from disk. + $nzbPath = $nzb->NZBPath($identifiers['g']); + if (! empty($nzbPath)) { + File::delete($nzbPath); + } + + // Delete images. + $releaseImage->delete($identifiers['g']); + + if (config('nntmux.elasticsearch_enabled') === true) { + if ($identifiers['i'] === false) { + $identifiers['i'] = Release::query()->where('guid', $identifiers['g'])->first(['id']); + if ($identifiers['i'] !== null) { + $identifiers['i'] = $identifiers['i']['id']; + } + } if ($identifiers['i'] !== null) { - $identifiers['i'] = $identifiers['i']['id']; - } - } - if ($identifiers['i'] !== null) { - $params = [ - 'index' => 'releases', - 'id' => $identifiers['i'], - ]; + $params = [ + 'index' => 'releases', + 'id' => $identifiers['i'], + ]; - try { - Elasticsearch::delete($params); - } catch (Missing404Exception $e) { - // we do nothing here just catch the error, we don't care if release is missing from ES, we are deleting it anyway + try { + Elasticsearch::delete($params); + } catch (Missing404Exception $e) { + // we do nothing here just catch the error, we don't care if release is missing from ES, we are deleting it anyway + } } + } else { + // Delete from sphinx. + $this->manticoreSearch->deleteRelease($identifiers); } - } else { - // Delete from sphinx. - $this->manticoreSearch->deleteRelease($identifiers); + + // Delete from DB. + self::whereGuid($identifiers['g'])->delete(); } - // Delete from DB. - self::whereGuid($identifiers['g'])->delete(); - } + /** + * @return bool|int + */ + public function updateMulti($guids, $category, $grabs, $videoId, $episodeId, $anidbId, $imdbId) + { + if (! \is_array($guids) || \count($guids) < 1) { + return false; + } - /** - * @return bool|int - */ - public function updateMulti($guids, $category, $grabs, $videoId, $episodeId, $anidbId, $imdbId) - { - if (! \is_array($guids) || \count($guids) < 1) { - return false; + $update = [ + 'categories_id' => $category === -1 ? 'categories_id' : $category, + 'grabs' => $grabs, + 'videos_id' => $videoId, + 'tv_episodes_id' => $episodeId, + 'anidbid' => $anidbId, + 'imdbid' => $imdbId, + ]; + + return self::query()->whereIn('guid', $guids)->update($update); } - $update = [ - 'categories_id' => $category === -1 ? 'categories_id' : $category, - 'grabs' => $grabs, - 'videos_id' => $videoId, - 'tv_episodes_id' => $episodeId, - 'anidbid' => $anidbId, - 'imdbid' => $imdbId, - ]; - - return self::query()->whereIn('guid', $guids)->update($update); - } - - /** - * 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]); + /** + * 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 .= ') '; - } - $sql .= ') '; - return $sql; - } - - /** - * Function for searching on the site (by subject, searchname or advanced). - * - * @return array|Collection|mixed - */ - public function search(array $searchArr, $groupName, $sizeFrom, $sizeTo, $daysNew, $daysOld, int $offset = 0, int $limit = 1000, array|string $orderBy = '', int $maxAge = -1, array $excludedCats = [], string $type = 'basic', array $cat = [-1], int $minSize = 0): mixed - { - $sizeRange = [ - 1 => 1, - 2 => 2.5, - 3 => 5, - 4 => 10, - 5 => 20, - 6 => 30, - 7 => 40, - 8 => 80, - 9 => 160, - 10 => 320, - 11 => 640, - ]; - - if ($orderBy === '') { - $orderBy = ['postdate', 'desc']; - } else { - $orderBy = $this->getBrowseOrder($orderBy); + return $sql; } - $searchFields = Arr::where($searchArr, static function ($value) { - return $value !== -1; - }); - - $phrases = array_values($searchFields); - - if (config('nntmux.elasticsearch_enabled') === true) { - $searchResult = $this->elasticSearch->indexSearch($phrases, $limit); - } else { - $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', '', [], $searchFields); - if (! empty($searchResult)) { - $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); + /** + * Function for searching on the site (by subject, searchname or advanced). + * + * + * @return array|Collection|mixed + */ + public function search(array $searchArr, $groupName, $sizeFrom, $sizeTo, $daysNew, $daysOld, int $offset = 0, int $limit = 1000, array|string $orderBy = '', int $maxAge = -1, array $excludedCats = [], string $type = 'basic', array $cat = [-1], int $minSize = 0): mixed + { + $sizeRange = [ + 1 => 1, + 2 => 2.5, + 3 => 5, + 4 => 10, + 5 => 20, + 6 => 30, + 7 => 40, + 8 => 80, + 9 => 160, + 10 => 320, + 11 => 640, + ]; + if ($orderBy === '') { + $orderBy = []; + $orderBy[0] = 'postdate '; + $orderBy[1] = 'desc '; + } else { + $orderBy = $this->getBrowseOrder($orderBy); } - } - if (count($searchResult) === 0) { - return collect(); - } - - $query = self::query() - ->with(['group', 'category', 'category.parent', 'video', 'video.episode', 'nfo', 'failed']) - ->where('nzbstatus', NZB::NZB_ADDED) - ->where('passwordstatus', $this->showPasswords()) - ->whereIn('id', $searchResult); - - if ($type === 'basic') { - $categories = Category::getCategorySearch($cat, null, true); - if ($categories !== null) { - $query->whereIn('categories_id', Arr::wrap($categories)); - } - } elseif ($type === 'advanced' && (int) $cat[0] !== -1) { - $query->where('categories_id', $cat[0]); - } - - if ($maxAge > 0) { - $query->where('postdate', '>', now()->subDays($maxAge)); - } - - if (! empty($excludedCats)) { - $query->whereNotIn('categories_id', $excludedCats); - } - - if ((int) $groupName !== -1) { - $query->whereHas('group', function ($q) use ($groupName) { - $q->where('name', $groupName); + $searchFields = Arr::where($searchArr, static function ($value) { + return $value !== -1; }); - } - if ($sizeFrom > 0 && array_key_exists($sizeFrom, $sizeRange)) { - $query->where('size', '>', 104857600 * (int) $sizeRange[$sizeFrom]); - } + $phrases = array_values($searchFields); - if ($sizeTo > 0 && array_key_exists($sizeTo, $sizeRange)) { - $query->where('size', '<', 104857600 * (int) $sizeRange[$sizeTo]); - } - - if ($daysNew !== -1) { - $query->where('postdate', '<', now()->subDays($daysNew)); - } - - if ($daysOld !== -1) { - $query->where('postdate', '>', now()->subDays($daysOld)); - } - - if ($minSize > 0) { - $query->where('size', '>=', $minSize); - } - - $query->orderBy($orderBy[0], $orderBy[1]) - ->offset($offset) - ->limit($limit); - - $cacheKey = md5($query->toRawSql()); - $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium')); - - $releases = Cache::get($cacheKey); - if ($releases !== null) { - return $releases; - } - - $releases = $query->get(); - - if ($releases->isNotEmpty()) { - $releases[0]->_totalrows = $query->count(); - } - - Cache::put($cacheKey, $releases, $cacheTTL); - - return $releases; - } - - /** - * Search function for API. - * - * @return Collection|mixed - */ - public function apiSearch($searchName, $groupName, int $offset = 0, int $limit = 1000, int $maxAge = -1, array $excludedCats = [], array $cat = [-1], int $minSize = 0): mixed - { - $query = self::query() - ->with(['video', 'video.episode', 'movieinfo', 'group', 'category', 'category.parent']) - ->where('nzbstatus', NZB::NZB_ADDED) - ->where('passwordstatus', $this->showPasswords()); - - if ($searchName !== -1) { if (config('nntmux.elasticsearch_enabled') === true) { - $searchResult = $this->elasticSearch->indexSearchApi($searchName, $limit); + $searchResult = $this->elasticSearch->indexSearch($phrases, $limit); } else { - $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $searchName, ['searchname']); - if (! empty($searchResult)) { - $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); - } - } - if (count($searchResult) !== 0) { - $query->whereIn('id', $searchResult); - } else { - return collect(); - } - } - - if ($maxAge > 0) { - $query->where('postdate', '>', now()->subDays($maxAge)); - } - - if (! empty($excludedCats)) { - $query->whereNotIn('categories_id', $excludedCats); - } - - if ((int) $groupName !== -1) { - $query->whereHas('group', function ($q) use ($groupName) { - $q->where('name', $groupName); - }); - } - - if ($cat !== [-1]) { - $query->whereIn('categories_id', $cat); - } - - if ($minSize > 0) { - $query->where('size', '>=', $minSize); - } - - $query->orderBy('postdate', 'desc') - ->offset($offset) - ->limit($limit); - - $cacheKey = md5($query->toRawSql()); - $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium')); - - $releases = Cache::get($cacheKey); - if ($releases !== null) { - return $releases; - } - - $releases = $query->get(); - - if ($releases->isNotEmpty()) { - $releases[0]->_totalrows = $query->count(); - } - - Cache::put($cacheKey, $releases, $cacheTTL); - - return $releases; - } - - /** - * Search for TV shows via API. - * - * @return array|\Illuminate\Cache\|Collection|\Illuminate\Support\Collection|mixed - */ - public function tvSearch(array $siteIdArr = [], string $series = '', string $episode = '', string $airDate = '', int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, int $minSize = 0, array $excludedCategories = []): mixed - { - $siteSQL = []; - $showSql = ''; - foreach ($siteIdArr as $column => $Id) { - if ($Id > 0) { - $siteSQL[] = sprintf('v.%s = %d', $column, $Id); - } - } - - if (\count($siteSQL) > 0) { - // If we have show info, find the Episode ID/Video ID first to avoid table scans - $showQry = sprintf( - " - SELECT - v.id AS video, - GROUP_CONCAT(tve.id SEPARATOR ',') AS episodes - FROM videos v - LEFT JOIN tv_episodes tve ON v.id = tve.videos_id - WHERE (%s) %s %s %s - GROUP BY v.id - LIMIT 1", - implode(' OR ', $siteSQL), - ($series !== '' ? sprintf('AND tve.series = %d', (int) preg_replace('/^s0*/i', '', $series)) : ''), - ($episode !== '' ? sprintf('AND tve.episode = %d', (int) preg_replace('/^e0*/i', '', $episode)) : ''), - ($airDate !== '' ? sprintf('AND DATE(tve.firstaired) = %s', escapeString($airDate)) : '') - ); - - $show = self::fromQuery($showQry); - - if ($show->isNotEmpty()) { - if ((! empty($episode) && ! empty($series)) && $show[0]->episodes !== '') { - $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series; - } elseif (! empty($episode) && $show[0]->episodes !== '') { - $showSql = sprintf('AND r.tv_episodes_id IN (%s)', $show[0]->episodes); - } elseif (! empty($series) && empty($episode)) { - // If $series is set but episode is not, return Season Packs and Episodes - $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series; - } - if ($show[0]->video > 0) { - $showSql .= ' AND r.videos_id = '.$show[0]->video; - } - } else { - // If we were passed Site ID Info and no match was found, do not run the query - return []; - } - } - - // If $name is set it is a fallback search, add available SxxExx/airdate info to the query - if (! empty($name) && $showSql === '') { - if (! empty($series) && (int) $series < 1900) { - $name .= sprintf(' S%s', str_pad($series, 2, '0', STR_PAD_LEFT)); - if (! empty($episode) && ! str_contains($episode, '/')) { - $name .= sprintf('E%s', str_pad($episode, 2, '0', STR_PAD_LEFT)); - } - // If season is not empty but episode is, add a wildcard to the search - if (empty($episode)) { - $name .= '*'; - } - } elseif (! empty($airDate)) { - $name .= sprintf(' %s', str_replace(['/', '-', '.', '_'], ' ', $airDate)); - } - } - if (! empty($name)) { - if (config('nntmux.elasticsearch_enabled') === true) { - $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit); - } else { - $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']); + $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', '', [], $searchFields); if (! empty($searchResult)) { $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); } @@ -671,22 +471,254 @@ class Releases extends Release if (empty($searchResult)) { return collect(); } + + $catQuery = ''; + if ($type === 'basic') { + $catQuery = Category::getCategorySearch($cat); + } elseif ($type === 'advanced' && (int) $cat[0] !== -1) { + $catQuery = sprintf('AND r.categories_id = %d', $cat[0]); + } + $whereSql = sprintf( + 'WHERE r.passwordstatus %s AND r.nzbstatus = %d %s %s %s %s %s %s %s %s %s %s', + $this->showPasswords(), + NZB::NZB_ADDED, + ($maxAge > 0 ? sprintf(' AND r.postdate > (NOW() - INTERVAL %d DAY) ', $maxAge) : ''), + ((int) $groupName !== -1 ? sprintf(' AND r.groups_id = %d ', UsenetGroup::getIDByName($groupName)) : ''), + (array_key_exists($sizeFrom, $sizeRange) ? ' AND r.size > '.(104857600 * (int) $sizeRange[$sizeFrom]).' ' : ''), + (array_key_exists($sizeTo, $sizeRange) ? ' AND r.size < '.(104857600 * (int) $sizeRange[$sizeTo]).' ' : ''), + $catQuery, + ((int) $daysNew !== -1 ? sprintf(' AND r.postdate < (NOW() - INTERVAL %d DAY) ', $daysNew) : ''), + ((int) $daysOld !== -1 ? sprintf(' AND r.postdate > (NOW() - INTERVAL %d DAY) ', $daysOld) : ''), + (\count($excludedCats) > 0 ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''), + ('AND r.id IN ('.implode(',', $searchResult).')'), + ($minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : '') + ); + $baseSql = sprintf( + "SELECT 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, + df.failed AS failed, + g.name AS group_name, + rn.releases_id AS nfoid, + re.releases_id AS reid, + cp.id AS categoryparentid, + v.tvdb, v.trakt, v.tvrage, v.tvmaze, v.imdb, v.tmdb, + tve.firstaired + FROM releases r + LEFT OUTER JOIN video_data re ON re.releases_id = r.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 release_nfos rn ON rn.releases_id = r.id + LEFT JOIN usenet_groups g ON g.id = r.groups_id + 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 dnzb_failures df ON df.release_id = r.id + %s", + $whereSql + ); + $sql = sprintf( + 'SELECT * FROM ( + %s + ) r + ORDER BY r.%s %s + LIMIT %d OFFSET %d', + $baseSql, + $orderBy[0], + $orderBy[1], + $limit, + $offset + ); + $releases = Cache::get(md5($sql)); + if ($releases !== null) { + return $releases; + } + $releases = self::fromQuery($sql); + if ($releases->isNotEmpty()) { + $releases[0]->_totalrows = $this->getPagerCount($baseSql); + } + $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); + Cache::put(md5($sql), $releases, $expiresAt); + + return $releases; } - $whereSql = sprintf( - 'WHERE r.nzbstatus = %d + + /** + * Search function for API. + * + * + * @return Collection|mixed + */ + public function apiSearch($searchName, $groupName, int $offset = 0, int $limit = 1000, int $maxAge = -1, array $excludedCats = [], array $cat = [-1], int $minSize = 0): mixed + { + if ($searchName !== -1) { + if (config('nntmux.elasticsearch_enabled') === true) { + $searchResult = $this->elasticSearch->indexSearchApi($searchName, $limit); + } else { + $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $searchName, ['searchname']); + if (! empty($searchResult)) { + $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); + } + } + } + + $catQuery = Category::getCategorySearch($cat); + + $whereSql = sprintf( + 'WHERE r.passwordstatus %s AND r.nzbstatus = %d %s %s %s %s %s %s', + $this->showPasswords(), + NZB::NZB_ADDED, + ($maxAge > 0 ? sprintf(' AND r.postdate > (NOW() - INTERVAL %d DAY) ', $maxAge) : ''), + ((int) $groupName !== -1 ? sprintf(' AND r.groups_id = %d ', UsenetGroup::getIDByName($groupName)) : ''), + $catQuery, + (\count($excludedCats) > 0 ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''), + (! empty($searchResult) ? 'AND r.id IN ('.implode(',', $searchResult).')' : ''), + ($minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : '') + ); + $baseSql = sprintf( + "SELECT 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, m.imdbid, m.tmdbid, m.traktid, cp.title AS parent_category, c.title AS sub_category, + CONCAT(cp.title, ' > ', c.title) AS category_name, + g.name AS group_name, + cp.id AS categoryparentid, + v.tvdb, v.trakt, v.tvrage, v.tvmaze, v.imdb, v.tmdb, + tve.firstaired, tve.title, tve.series, tve.episode + FROM releases r + 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 JOIN movieinfo m ON m.id = r.movieinfo_id + LEFT JOIN usenet_groups g ON g.id = r.groups_id + LEFT JOIN categories c ON c.id = r.categories_id + LEFT JOIN root_categories cp ON cp.id = c.root_categories_id + %s", + $whereSql + ); + $sql = sprintf( + 'SELECT * FROM ( + %s + ) r + ORDER BY r.postdate DESC + LIMIT %d OFFSET %d', + $baseSql, + $limit, + $offset + ); + $releases = Cache::get(md5($sql)); + if ($releases !== null) { + return $releases; + } + if ($searchName !== -1 && ! empty($searchResult)) { + $releases = self::fromQuery($sql); + } elseif ($searchName !== -1 && empty($searchResult)) { + $releases = collect(); + } elseif ($searchName === -1) { + $releases = self::fromQuery($sql); + } else { + $releases = collect(); + } + if ($releases->isNotEmpty()) { + $releases[0]->_totalrows = $this->getPagerCount($baseSql); + } + $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); + Cache::put(md5($sql), $releases, $expiresAt); + + return $releases; + } + + /** + * Search for TV shows via API. + * + * @return array|\Illuminate\Cache\|\Illuminate\Database\Eloquent\Collection|\Illuminate\Support\Collection|mixed + */ + public function tvSearch(array $siteIdArr = [], string $series = '', string $episode = '', string $airDate = '', int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, int $minSize = 0, array $excludedCategories = []): mixed + { + $siteSQL = []; + $showSql = ''; + foreach ($siteIdArr as $column => $Id) { + if ($Id > 0) { + $siteSQL[] = sprintf('v.%s = %d', $column, $Id); + } + } + + if (\count($siteSQL) > 0) { + // If we have show info, find the Episode ID/Video ID first to avoid table scans + $showQry = sprintf( + " + SELECT + v.id AS video, + GROUP_CONCAT(tve.id SEPARATOR ',') AS episodes + FROM videos v + LEFT JOIN tv_episodes tve ON v.id = tve.videos_id + WHERE (%s) %s %s %s + GROUP BY v.id + LIMIT 1", + implode(' OR ', $siteSQL), + ($series !== '' ? sprintf('AND tve.series = %d', (int) preg_replace('/^s0*/i', '', $series)) : ''), + ($episode !== '' ? sprintf('AND tve.episode = %d', (int) preg_replace('/^e0*/i', '', $episode)) : ''), + ($airDate !== '' ? sprintf('AND DATE(tve.firstaired) = %s', escapeString($airDate)) : '') + ); + + $show = self::fromQuery($showQry); + + if ($show->isNotEmpty()) { + if ((! empty($episode) && ! empty($series)) && $show[0]->episodes !== '') { + $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series; + } elseif (! empty($episode) && $show[0]->episodes !== '') { + $showSql = sprintf('AND r.tv_episodes_id IN (%s)', $show[0]->episodes); + } elseif (! empty($series) && empty($episode)) { + // If $series is set but episode is not, return Season Packs and Episodes + $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series; + } + if ($show[0]->video > 0) { + $showSql .= ' AND r.videos_id = '.$show[0]->video; + } + } else { + // If we were passed Site ID Info and no match was found, do not run the query + return []; + } + } + + // If $name is set it is a fallback search, add available SxxExx/airdate info to the query + if (! empty($name) && $showSql === '') { + if (! empty($series) && (int) $series < 1900) { + $name .= sprintf(' S%s', str_pad($series, 2, '0', STR_PAD_LEFT)); + if (! empty($episode) && ! str_contains($episode, '/')) { + $name .= sprintf('E%s', str_pad($episode, 2, '0', STR_PAD_LEFT)); + } + // If season is not empty but episode is, add a wildcard to the search + if (empty($episode)) { + $name .= '*'; + } + } elseif (! empty($airDate)) { + $name .= sprintf(' %s', str_replace(['/', '-', '.', '_'], ' ', $airDate)); + } + } + if (! empty($name)) { + if (config('nntmux.elasticsearch_enabled') === true) { + $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit); + } else { + $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']); + if (! empty($searchResult)) { + $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); + } + } + + if (empty($searchResult)) { + return collect(); + } + } + $whereSql = sprintf( + 'WHERE r.nzbstatus = %d AND r.passwordstatus %s %s %s %s %s %s %s', - NZB::NZB_ADDED, - $this->showPasswords(), - $showSql, - (! empty($name) && ! empty($searchResult)) ? 'AND r.id IN ('.implode(',', $searchResult).')' : '', - Category::getCategorySearch($cat, 'tv'), - $maxAge > 0 ? sprintf('AND r.postdate > NOW() - INTERVAL %d DAY', $maxAge) : '', - $minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : '', - ! empty($excludedCategories) ? sprintf('AND r.categories_id NOT IN('.implode(',', $excludedCategories).')') : '' - ); - $baseSql = sprintf( - "SELECT 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, + NZB::NZB_ADDED, + $this->showPasswords(), + $showSql, + (! empty($name) && ! empty($searchResult)) ? 'AND r.id IN ('.implode(',', $searchResult).')' : '', + Category::getCategorySearch($cat, 'tv'), + $maxAge > 0 ? sprintf('AND r.postdate > NOW() - INTERVAL %d DAY', $maxAge) : '', + $minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : '', + ! empty($excludedCategories) ? sprintf('AND r.categories_id NOT IN('.implode(',', $excludedCategories).')') : '' + ); + $baseSql = sprintf( + "SELECT 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, v.title, v.countries_id, v.started, v.tvdb, v.trakt, v.imdb, v.tmdb, v.tvmaze, v.tvrage, v.source, tvi.summary, tvi.publisher, tvi.image, @@ -705,283 +737,368 @@ class Releases extends Release LEFT OUTER JOIN video_data re ON re.releases_id = r.id LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id %s", - $whereSql - ); - $sql = sprintf( - '%s + $whereSql + ); + $sql = sprintf( + '%s ORDER BY postdate DESC LIMIT %d OFFSET %d', - $baseSql, - $limit, - $offset - ); - - $releases = Cache::get(md5($sql)); - if ($releases !== null) { - return $releases; - } - $releases = ((! empty($name) && ! empty($searchResult)) || empty($name)) ? self::fromQuery($sql) : []; - if (count($releases) !== 0 && $releases->isNotEmpty()) { - $releases[0]->_totalrows = $this->getPagerCount( - preg_replace('#LEFT(\s+OUTER)?\s+JOIN\s+(?!tv_episodes)\s+.*ON.*=.*\n#i', ' ', $baseSql) + $baseSql, + $limit, + $offset ); + $releases = Cache::get(md5($sql)); + if ($releases !== null) { + return $releases; + } + $releases = ((! empty($name) && ! empty($searchResult)) || empty($name)) ? self::fromQuery($sql) : []; + if (count($releases) !== 0 && $releases->isNotEmpty()) { + $releases[0]->_totalrows = $this->getPagerCount( + preg_replace('#LEFT(\s+OUTER)?\s+JOIN\s+(?!tv_episodes)\s+.*ON.*=.*\n#i', ' ', $baseSql) + ); + } + $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); + Cache::put(md5($sql), $releases, $expiresAt); + + return $releases; } - $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); - Cache::put(md5($sql), $releases, $expiresAt); - return $releases; - } - - /** - * Search TV Shows via APIv2. - * - * @return Collection|mixed - */ - public function apiTvSearch(array $siteIdArr = [], string $series = '', string $episode = '', string $airDate = '', int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, int $minSize = 0, array $excludedCategories = []): mixed - { - $query = Release::query() - ->with(['video', 'video.episode', 'category', 'category.parent', 'group']) - ->where('nzbstatus', NZB::NZB_ADDED) - ->where('passwordstatus', $this->showPasswords()) - ->whereIn('categories_id', Category::getCategorySearch($cat, 'tv', true)); - - // Check if siteIdArr contains id key - if (! empty($siteIdArr) && array_key_exists('id', $siteIdArr) && $siteIdArr['id'] > 0) { - $query->where('videos_id', $siteIdArr['id']); - } - if (! empty($series)) { - $query->whereHas('episode', function ($q) use ($series, $episode, $airDate) { - $q->where('series', (int) preg_replace('/^s0*/i', '', $series)); - if (! empty($episode)) { - $q->where('episode', (int) preg_replace('/^e0*/i', '', $episode)); + /** + * Search TV Shows via APIv2. + * + * + * @return Collection|mixed + */ + public function apiTvSearch(array $siteIdArr = [], string $series = '', string $episode = '', string $airDate = '', int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, int $minSize = 0, array $excludedCategories = []): mixed + { + $siteSQL = []; + $showSql = ''; + foreach ($siteIdArr as $column => $Id) { + if ($Id > 0) { + $siteSQL[] = sprintf('v.%s = %d', $column, $Id); } - if (! empty($airDate)) { - $q->whereDate('firstaired', $airDate); - } - }); - } + } - if (! empty(array_filter($siteIdArr))) { - $query->whereHas('video', function ($q) use ($siteIdArr) { - foreach ($siteIdArr as $column => $id) { - if ($id > 0 && $column !== 'id') { - $q->orWhere($column, $id); + if (\count($siteSQL) > 0) { + // If we have show info, find the Episode ID/Video ID first to avoid table scans + $showQry = sprintf( + " + SELECT + v.id AS video, + GROUP_CONCAT(tve.id SEPARATOR ',') AS episodes + FROM videos v + LEFT JOIN tv_episodes tve ON v.id = tve.videos_id + WHERE (%s) %s %s %s + GROUP BY v.id + LIMIT 1", + implode(' OR ', $siteSQL), + ($series !== '' ? sprintf('AND tve.series = %d', (int) preg_replace('/^s0*/i', '', $series)) : ''), + ($episode !== '' ? sprintf('AND tve.episode = %d', (int) preg_replace('/^e0*/i', '', $episode)) : ''), + ($airDate !== '' ? sprintf('AND DATE(tve.firstaired) = %s', escapeString($airDate)) : '') + ); + + $show = self::fromQuery($showQry); + if ($show->isNotEmpty()) { + if ((! empty($episode) && ! empty($series)) && $show[0]->episodes !== '') { + $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series; + } elseif (! empty($episode) && $show[0]->episodes !== '') { + $showSql = sprintf('AND r.tv_episodes_id IN (%s)', $show[0]->episodes); + } elseif (! empty($series) && empty($episode)) { + // If $series is set but episode is not, return Season Packs and Episodes + $showSql .= ' AND r.tv_episodes_id IN ('.$show[0]->episodes.') AND tve.series = '.$series; } - + if ($show[0]->video > 0) { + $showSql .= ' AND r.videos_id = '.$show[0]->video; + } + } else { + // If we were passed Site ID Info and no match was found, do not run the query + return []; } - }); - } - - if (! empty($name)) { + } // If $name is set it is a fallback search, add available SxxExx/airdate info to the query - if (! empty($series) && (int) $series < 1900) { - $name .= sprintf(' S%s', str_pad($series, 2, '0', STR_PAD_LEFT)); - if (! empty($episode) && ! str_contains($episode, '/')) { - $name .= sprintf('E%s', str_pad($episode, 2, '0', STR_PAD_LEFT)); - } - // If season is not empty but episode is, add a wildcard to the search - if (empty($episode)) { - $name .= '*'; - } - } elseif (! empty($airDate)) { - $name .= sprintf(' %s', str_replace(['/', '-', '.', '_'], ' ', $airDate)); - } - - if (config('nntmux.elasticsearch_enabled') === true) { - $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit); - } else { - $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']); - if (! empty($searchResult)) { - $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); + if (! empty($name) && $showSql === '') { + if (! empty($series) && (int) $series < 1900) { + $name .= sprintf(' S%s', str_pad($series, 2, '0', STR_PAD_LEFT)); + if (! empty($episode) && ! str_contains($episode, '/')) { + $name .= sprintf('E%s', str_pad($episode, 2, '0', STR_PAD_LEFT)); + } + // If season is not empty but episode is, add a wildcard to the search + if (empty($episode)) { + $name .= '*'; + } + } elseif (! empty($airDate)) { + $name .= sprintf(' %s', str_replace(['/', '-', '.', '_'], ' ', $airDate)); } } + if (! empty($name)) { + if (config('nntmux.elasticsearch_enabled') === true) { + $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit); + } else { + $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']); + if (! empty($searchResult)) { + $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); + } + } - if (count($searchResult) === 0) { - return collect(); + if (empty($searchResult)) { + return collect(); + } } + $whereSql = sprintf( + 'WHERE r.nzbstatus = %d + AND r.passwordstatus %s + %s %s %s %s %s %s', + NZB::NZB_ADDED, + $this->showPasswords(), + $showSql, + (! empty($searchResult) ? 'AND r.id IN ('.implode(',', $searchResult).')' : ''), + Category::getCategorySearch($cat, 'tv'), + ($maxAge > 0 ? sprintf('AND r.postdate > NOW() - INTERVAL %d DAY', $maxAge) : ''), + ($minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : ''), + ! empty($excludedCategories) ? sprintf('AND r.categories_id NOT IN('.implode(',', $excludedCategories).')') : '' + ); + $baseSql = sprintf( + "SELECT 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.tv_episodes_id, r.haspreview, r.jpgstatus, + v.title, v.type, v.tvdb, v.trakt,v.imdb, v.tmdb, v.tvmaze, v.tvrage, + tve.series, tve.episode, tve.se_complete, tve.title, tve.firstaired, cp.title AS parent_category, c.title AS sub_category, + CONCAT(cp.title, ' > ', c.title) AS category_name, + g.name AS group_name + FROM releases r + LEFT OUTER JOIN videos v ON r.videos_id = v.id AND v.type = 0 + LEFT OUTER JOIN tv_info tvi ON v.id = tvi.videos_id + LEFT OUTER JOIN tv_episodes tve ON r.tv_episodes_id = tve.id + LEFT JOIN categories c ON c.id = r.categories_id + LEFT JOIN root_categories cp ON cp.id = c.root_categories_id + LEFT JOIN usenet_groups g ON g.id = r.groups_id + %s", + $whereSql + ); + $sql = sprintf( + '%s + ORDER BY postdate DESC + LIMIT %d OFFSET %d', + $baseSql, + $limit, + $offset + ); + $releases = Cache::get(md5($sql)); + if ($releases !== null) { + return $releases; + } + $releases = self::fromQuery($sql); + if ($releases->isNotEmpty()) { + $releases[0]->_totalrows = $this->getPagerCount( + preg_replace('#LEFT(\s+OUTER)?\s+JOIN\s+(?!tv_episodes)\s+.*ON.*=.*\n#i', ' ', $baseSql) + ); + } + $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); + Cache::put(md5($sql), $releases, $expiresAt); - $query->whereIn('id', $searchResult); - } - - if ($maxAge > 0) { - $query->where('postdate', '>', now()->subDays($maxAge)); - } - - if (! empty($excludedCategories)) { - $query->whereNotIn('categories_id', $excludedCategories); - } - - if ($cat !== [-1]) { - $query->whereIn('categories_id', $cat); - } - - if ($minSize > 0) { - $query->where('size', '>=', $minSize); - } - - $query->orderBy('postdate', 'desc') - ->offset($offset) - ->limit($limit); - - $cacheKey = md5($query->toRawSql()); - $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium')); - - $releases = Cache::get($cacheKey); - if ($releases !== null) { return $releases; } - $releases = $query->get(); + /** + * Search anime releases. + * + * + * @return Collection|mixed + */ + public function animeSearch($aniDbID, int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, array $excludedCategories = []): mixed + { + if (! empty($name)) { + if (config('nntmux.elasticsearch_enabled') === true) { + $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit); + } else { + $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']); + if (! empty($searchResult)) { + $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); + } + } - if ($releases->isNotEmpty()) { - $releases[0]->_totalrows = $query->count(); - } - - Cache::put($cacheKey, $releases, $cacheTTL); - - return $releases; - } - - public function moviesSearch(int $imDbId = -1, int $tmDbId = -1, int $traktId = -1, int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, int $minSize = 0, array $excludedCategories = []): mixed - { - $query = self::query() - ->with(['movieinfo', 'group', 'category', 'category.parent', 'nfo']) - ->whereBetween('categories_id', [Category::MOVIE_ROOT, Category::MOVIE_OTHER]) - ->where('nzbstatus', NZB::NZB_ADDED) - ->where('passwordstatus', $this->showPasswords()); - - if (! empty($name)) { - if (config('nntmux.elasticsearch_enabled') === true) { - $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit); - } else { - $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']); - if (! empty($searchResult)) { - $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); + if (empty($searchResult)) { + return collect(); } } - if (count($searchResult) === 0) { - return collect(); + $whereSql = sprintf( + 'WHERE r.passwordstatus %s + AND r.nzbstatus = %d + %s %s %s %s %s', + $this->showPasswords(), + NZB::NZB_ADDED, + ($aniDbID > -1 ? sprintf(' AND r.anidbid = %d ', $aniDbID) : ''), + (! empty($searchResult) ? 'AND r.id IN ('.implode(',', $searchResult).')' : ''), + ! empty($excludedCategories) ? sprintf('AND r.categories_id NOT IN('.implode(',', $excludedCategories).')') : '', + Category::getCategorySearch($cat), + ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : '') + ); + $baseSql = 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.haspreview, r.jpgstatus, cp.title AS parent_category, c.title AS sub_category, + CONCAT(cp.title, ' > ', c.title) AS category_name, + g.name AS group_name, + rn.releases_id AS nfoid + 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 + LEFT JOIN usenet_groups g ON g.id = r.groups_id + LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id + %s", + $whereSql + ); + $sql = sprintf( + '%s + ORDER BY postdate DESC + LIMIT %d OFFSET %d', + $baseSql, + $limit, + $offset + ); + $releases = Cache::get(md5($sql)); + if ($releases !== null) { + return $releases; } + $releases = self::fromQuery($sql); + if ($releases->isNotEmpty()) { + $releases[0]->_totalrows = $this->getPagerCount($baseSql); + } + $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); + Cache::put(md5($sql), $releases, $expiresAt); - $query->whereIn('id', $searchResult); - } - - if ($imDbId !== -1 && is_numeric($imDbId)) { - $query->whereHas('movieinfo', function ($q) use ($imDbId) { - $q->where('imdbid', $imDbId); - }); - } - - if ($tmDbId !== -1 && is_numeric($tmDbId)) { - $query->whereHas('movieinfo', function ($q) use ($tmDbId) { - $q->where('tmdbid', $tmDbId); - }); - } - - if ($traktId !== -1 && is_numeric($traktId)) { - $query->whereHas('movieinfo', function ($q) use ($traktId) { - $q->where('traktid', $traktId); - }); - } - - if (! empty($excludedCategories)) { - $query->whereNotIn('categories_id', $excludedCategories); - } - - if ($cat !== [-1]) { - $query->whereIn('categories_id', $cat); - } - - if ($maxAge > 0) { - $query->where('postdate', '>', now()->subDays($maxAge)); - } - - if ($minSize > 0) { - $query->where('size', '>=', $minSize); - } - - $totalRows = $query->count(); - - $query->orderBy('postdate', 'desc') - ->offset($offset) - ->limit($limit); - - $cacheKey = md5($query->toRawSql()); - $cacheTTL = now()->addMinutes(config('nntmux.cache_expiry_medium')); - - $releases = Cache::get($cacheKey); - if ($releases !== null) { return $releases; } - $releases = $query->get(); + /** + * Movies search through API and site. + * + * + * @return Collection|mixed + */ + public function moviesSearch(int $imDbId = -1, int $tmDbId = -1, int $traktId = -1, int $offset = 0, int $limit = 100, string $name = '', array $cat = [-1], int $maxAge = -1, int $minSize = 0, array $excludedCategories = []): mixed + { + if (! empty($name)) { + if (config('nntmux.elasticsearch_enabled') === true) { + $searchResult = $this->elasticSearch->indexSearchTMA($name, $limit); + } else { + $searchResult = $this->manticoreSearch->searchIndexes('releases_rt', $name, ['searchname']); + if (! empty($searchResult)) { + $searchResult = Arr::wrap(Arr::get($searchResult, 'id')); + } + } - if ($releases->isNotEmpty()) { - $releases[0]->_totalrows = $totalRows; - } - - Cache::put($cacheKey, $releases, $cacheTTL); - - return $releases; - } - - public function searchSimilar($currentID, $name, array $excludedCats = []): bool|array - { - // Get the category for the parent of this release. - $ret = false; - $currRow = self::getCatByRelId($currentID); - if ($currRow !== null) { - $catRow = Category::find($currRow['categories_id']); - $parentCat = $catRow !== null ? $catRow['root_categories_id'] : null; - - if ($parentCat === null) { - return $ret; - } - - $results = $this->search(['searchname' => getSimilarName($name)], -1, '', '', -1, -1, 0, config('nntmux.items_per_page'), '', -1, $excludedCats, 'basic', [$parentCat]); - if (! $results) { - return $ret; - } - - $ret = []; - foreach ($results as $res) { - if ($res['id'] !== $currentID && $res['categoryparentid'] === $parentCat) { - $ret[] = $res; + if (empty($searchResult)) { + return collect(); } } + + $whereSql = sprintf( + 'WHERE r.categories_id BETWEEN '.Category::MOVIE_ROOT.' AND '.Category::MOVIE_OTHER.' + AND r.nzbstatus = %d + AND r.passwordstatus %s + %s %s %s %s %s %s %s', + NZB::NZB_ADDED, + $this->showPasswords(), + (! empty($searchResult) ? 'AND r.id IN ('.implode(',', $searchResult).')' : ''), + ($imDbId !== -1 && is_numeric($imDbId)) ? sprintf(' AND m.imdbid = \'%s\' ', $imDbId) : '', + ($tmDbId !== -1 && is_numeric($tmDbId)) ? sprintf(' AND m.tmdbid = %d ', $tmDbId) : '', + ($traktId !== -1 && is_numeric($traktId)) ? sprintf(' AND m.traktid = %d ', $traktId) : '', + ! empty($excludedCategories) ? sprintf('AND r.categories_id NOT IN('.implode(',', $excludedCategories).')') : '', + Category::getCategorySearch($cat, 'movies'), + $maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : '', + $minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : '' + ); + $baseSql = 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.imdbid, r.videos_id, r.tv_episodes_id, r.haspreview, r.jpgstatus, m.imdbid, m.tmdbid, m.traktid, cp.title AS parent_category, c.title AS sub_category, + concat(cp.title, ' > ', c.title) AS category_name, + g.name AS group_name, + rn.releases_id AS nfoid + FROM releases r + LEFT JOIN movieinfo m ON m.id = r.movieinfo_id + LEFT JOIN usenet_groups g ON g.id = r.groups_id + 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 release_nfos rn ON rn.releases_id = r.id + %s", + $whereSql + ); + $sql = sprintf( + '%s + ORDER BY postdate DESC + LIMIT %d OFFSET %d', + $baseSql, + $limit, + $offset + ); + + $releases = Cache::get(md5($sql)); + if ($releases !== null) { + return $releases; + } + $releases = self::fromQuery($sql); + if ($releases->isNotEmpty()) { + $releases[0]->_totalrows = $this->getPagerCount($baseSql); + } + $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); + Cache::put(md5($sql), $releases, $expiresAt); + + return $releases; } - return $ret; - } + public function searchSimilar($currentID, $name, array $excludedCats = []): bool|array + { + // Get the category for the parent of this release. + $ret = false; + $currRow = self::getCatByRelId($currentID); + if ($currRow !== null) { + $catRow = Category::find($currRow['categories_id']); + $parentCat = $catRow !== null ? $catRow['root_categories_id'] : null; - /** - * Get count of releases for pager. - * - * @param string $query The query to get the count from. - */ - private function getPagerCount(string $query): int - { - $queryBuilder = DB::table(DB::raw('('.preg_replace( - '/SELECT.+?FROM\s+releases/is', - 'SELECT r.id FROM releases', - $query - ).' LIMIT '.(int) config('nntmux.max_pager_results').') as z')) - ->selectRaw('COUNT(z.id) as count'); + if ($parentCat === null) { + return $ret; + } - $sql = $queryBuilder->toSql(); - $count = Cache::get(md5($sql)); + $results = $this->search(['searchname' => getSimilarName($name)], -1, '', '', -1, -1, 0, config('nntmux.items_per_page'), '', -1, $excludedCats, 'basic', [$parentCat]); + if (! $results) { + return $ret; + } + + $ret = []; + foreach ($results as $res) { + if ($res['id'] !== $currentID && $res['categoryparentid'] === $parentCat) { + $ret[] = $res; + } + } + } + + return $ret; + } + + /** + * Get count of releases for pager. + * + * @param string $query The query to get the count from. + */ + private function getPagerCount(string $query): int + { + $queryBuilder = DB::table(DB::raw('('.preg_replace( + '/SELECT.+?FROM\s+releases/is', + 'SELECT r.id FROM releases', + $query + ).' LIMIT '.(int) config('nntmux.max_pager_results').') as z')) + ->selectRaw('COUNT(z.id) as count'); + + $sql = $queryBuilder->toSql(); + $count = Cache::get(md5($sql)); + + if ($count !== null) { + return $count; + } + + $result = $queryBuilder->first(); + $count = $result->count ?? 0; + + $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_short')); + Cache::put(md5($sql), $count, $expiresAt); - if ($count !== null) { return $count; } - - $result = $queryBuilder->first(); - $count = $result->count ?? 0; - - $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_short')); - Cache::put(md5($sql), $count, $expiresAt); - - return $count; } -} diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index 30baa3b0b..f297e99c5 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -1,500 +1,500 @@ has('t')) { - switch ($request->input('t')) { - case 'd': - case 'details': - $function = 'd'; - break; - case 'g': - case 'get': - $function = 'g'; - break; - case 's': - case 'search': - break; - case 'c': - case 'caps': - $function = 'c'; - break; - case 'tv': - case 'tvsearch': - $function = 'tv'; - break; - case 'm': - case 'movie': - $function = 'm'; - break; - case 'gn': - case 'n': - case 'nfo': - case 'info': - $function = 'n'; - break; - default: - return Utility::showApiError(202, 'No such function ('.$request->input('t').')'); - } - } else { - return Utility::showApiError(200, 'Missing parameter (t)'); - } + private string $type; - $uid = $apiKey = $oldestGrabTime = $thisOldestTime = ''; - $res = $catExclusions = []; - $maxRequests = $thisRequests = $maxDownloads = $grabs = 0; - - // Page is accessible only by the apikey - - if ($function !== 'c' && $function !== 'r') { - if ($request->missing('apikey') || ($request->has('apikey') && empty($request->input('apikey')))) { - return Utility::showApiError(200, 'Missing parameter (apikey)'); + /** + * @return Application|\Illuminate\Foundation\Application|RedirectResponse|Redirector|StreamedResponse|void + * + * @throws \Throwable + */ + public function api(Request $request) + { + // API functions. + $function = 's'; + if ($request->has('t')) { + switch ($request->input('t')) { + case 'd': + case 'details': + $function = 'd'; + break; + case 'g': + case 'get': + $function = 'g'; + break; + case 's': + case 'search': + break; + case 'c': + case 'caps': + $function = 'c'; + break; + case 'tv': + case 'tvsearch': + $function = 'tv'; + break; + case 'm': + case 'movie': + $function = 'm'; + break; + case 'gn': + case 'n': + case 'nfo': + case 'info': + $function = 'n'; + break; + default: + return Utility::showApiError(202, 'No such function ('.$request->input('t').')'); + } } else { - $apiKey = $request->input('apikey'); - $res = User::getByRssToken($apiKey); - if ($res === null) { - return Utility::showApiError(100, 'Incorrect user credentials (wrong API key)'); - } + return Utility::showApiError(200, 'Missing parameter (t)'); } - if ($res->hasRole('Disabled')) { - return Utility::showApiError(101); - } + $uid = $apiKey = $oldestGrabTime = $thisOldestTime = ''; + $res = $catExclusions = []; + $maxRequests = $thisRequests = $maxDownloads = $grabs = 0; - $uid = $res->id; - $catExclusions = User::getCategoryExclusionForApi($request); - $maxRequests = $res->role->apirequests; - $maxDownloads = $res->role->downloadrequests; - $time = UserRequest::whereUsersId($uid)->min('timestamp'); - $thisOldestTime = $time !== null ? Carbon::createFromTimeString($time)->toRfc2822String() : ''; - $grabTime = UserDownload::whereUsersId($uid)->min('timestamp'); - $oldestGrabTime = $grabTime !== null ? Carbon::createFromTimeString($grabTime)->toRfc2822String() : ''; - } + // Page is accessible only by the apikey - // Record user access to the api, if its been called by a user (i.e. capabilities request do not require a user to be logged in or key provided). - if ($uid !== '') { - event(new UserAccessedApi($res)); - $thisRequests = UserRequest::getApiRequests($uid); - $grabs = UserDownload::getDownloadRequests($uid); - if ($thisRequests > $maxRequests) { - return Utility::showApiError(500, 'Request limit reached ('.$thisRequests.'/'.$maxRequests.')'); - } - } - - $releases = new Releases; - - // Set Query Parameters based on Request objects - $outputXML = ! ($request->has('o') && $request->input('o') === 'json'); - $minSize = $request->has('minsize') && $request->input('minsize') > 0 ? $request->input('minsize') : 0; - $offset = $this->offset($request); - - // Set API Parameters based on Request objects - $params['extended'] = $request->has('extended') && (int) $request->input('extended') === 1 ? '1' : '0'; - $params['del'] = $request->has('del') && (int) $request->input('del') === 1 ? '1' : '0'; - $params['uid'] = $uid; - $params['token'] = $apiKey; - $params['apilimit'] = $maxRequests; - $params['requests'] = $thisRequests; - $params['downloadlimit'] = $maxDownloads; - $params['grabs'] = $grabs; - $params['oldestapi'] = $thisOldestTime; - $params['oldestgrab'] = $oldestGrabTime; - - switch ($function) { - // Search releases. - case 's': - $this->verifyEmptyParameter($request, 'q'); - $maxAge = $this->maxAge($request); - $groupName = $this->group($request); - UserRequest::addApiRequest($apiKey, $request->getRequestUri()); - $categoryID = $this->categoryID($request); - $limit = $this->limit($request); - $searchArr = [ - 'searchname' => $request->input('q') ?? -1, - 'name' => -1, - 'fromname' => -1, - 'filename' => -1, - ]; - - if ($request->has('q')) { - $relData = $releases->search( - $searchArr, - $groupName, - -1, - -1, - -1, - -1, - $offset, - $limit, - '', - $maxAge, - $catExclusions, - 'basic', - $categoryID, - $minSize - ); + if ($function !== 'c' && $function !== 'r') { + if ($request->missing('apikey') || ($request->has('apikey') && empty($request->input('apikey')))) { + return Utility::showApiError(200, 'Missing parameter (apikey)'); } else { - $relData = $releases->getBrowseRange( - 1, - $categoryID, - $offset, - $limit, - '', - $maxAge, - $catExclusions, - $groupName, - $minSize - ); + $apiKey = $request->input('apikey'); + $res = User::getByRssToken($apiKey); + if ($res === null) { + return Utility::showApiError(100, 'Incorrect user credentials (wrong API key)'); + } } - $this->output($relData, $params, $outputXML, $offset, 'api'); - break; + + if ($res->hasRole('Disabled')) { + return Utility::showApiError(101); + } + + $uid = $res->id; + $catExclusions = User::getCategoryExclusionForApi($request); + $maxRequests = $res->role->apirequests; + $maxDownloads = $res->role->downloadrequests; + $time = UserRequest::whereUsersId($uid)->min('timestamp'); + $thisOldestTime = $time !== null ? Carbon::createFromTimeString($time)->toRfc2822String() : ''; + $grabTime = UserDownload::whereUsersId($uid)->min('timestamp'); + $oldestGrabTime = $grabTime !== null ? Carbon::createFromTimeString($grabTime)->toRfc2822String() : ''; + } + + // Record user access to the api, if its been called by a user (i.e. capabilities request do not require a user to be logged in or key provided). + if ($uid !== '') { + event(new UserAccessedApi($res)); + $thisRequests = UserRequest::getApiRequests($uid); + $grabs = UserDownload::getDownloadRequests($uid); + if ($thisRequests > $maxRequests) { + return Utility::showApiError(500, 'Request limit reached ('.$thisRequests.'/'.$maxRequests.')'); + } + } + + $releases = new Releases; + + // Set Query Parameters based on Request objects + $outputXML = ! ($request->has('o') && $request->input('o') === 'json'); + $minSize = $request->has('minsize') && $request->input('minsize') > 0 ? $request->input('minsize') : 0; + $offset = $this->offset($request); + + // Set API Parameters based on Request objects + $params['extended'] = $request->has('extended') && (int) $request->input('extended') === 1 ? '1' : '0'; + $params['del'] = $request->has('del') && (int) $request->input('del') === 1 ? '1' : '0'; + $params['uid'] = $uid; + $params['token'] = $apiKey; + $params['apilimit'] = $maxRequests; + $params['requests'] = $thisRequests; + $params['downloadlimit'] = $maxDownloads; + $params['grabs'] = $grabs; + $params['oldestapi'] = $thisOldestTime; + $params['oldestgrab'] = $oldestGrabTime; + + switch ($function) { + // Search releases. + case 's': + $this->verifyEmptyParameter($request, 'q'); + $maxAge = $this->maxAge($request); + $groupName = $this->group($request); + UserRequest::addApiRequest($apiKey, $request->getRequestUri()); + $categoryID = $this->categoryID($request); + $limit = $this->limit($request); + $searchArr = [ + 'searchname' => $request->input('q') ?? -1, + 'name' => -1, + 'fromname' => -1, + 'filename' => -1, + ]; + + if ($request->has('q')) { + $relData = $releases->search( + $searchArr, + $groupName, + -1, + -1, + -1, + -1, + $offset, + $limit, + '', + $maxAge, + $catExclusions, + 'basic', + $categoryID, + $minSize + ); + } else { + $relData = $releases->getBrowseRange( + 1, + $categoryID, + $offset, + $limit, + '', + $maxAge, + $catExclusions, + $groupName, + $minSize + ); + } + $this->output($relData, $params, $outputXML, $offset, 'api'); + break; // Search tv releases. - case 'tv': - $this->verifyEmptyParameter($request, 'q'); - $this->verifyEmptyParameter($request, 'vid'); - $this->verifyEmptyParameter($request, 'tvdbid'); - $this->verifyEmptyParameter($request, 'traktid'); - $this->verifyEmptyParameter($request, 'rid'); - $this->verifyEmptyParameter($request, 'tvmazeid'); - $this->verifyEmptyParameter($request, 'imdbid'); - $this->verifyEmptyParameter($request, 'tmdbid'); - $this->verifyEmptyParameter($request, 'season'); - $this->verifyEmptyParameter($request, 'ep'); - $maxAge = $this->maxAge($request); - UserRequest::addApiRequest($apiKey, $request->getRequestUri()); + case 'tv': + $this->verifyEmptyParameter($request, 'q'); + $this->verifyEmptyParameter($request, 'vid'); + $this->verifyEmptyParameter($request, 'tvdbid'); + $this->verifyEmptyParameter($request, 'traktid'); + $this->verifyEmptyParameter($request, 'rid'); + $this->verifyEmptyParameter($request, 'tvmazeid'); + $this->verifyEmptyParameter($request, 'imdbid'); + $this->verifyEmptyParameter($request, 'tmdbid'); + $this->verifyEmptyParameter($request, 'season'); + $this->verifyEmptyParameter($request, 'ep'); + $maxAge = $this->maxAge($request); + UserRequest::addApiRequest($apiKey, $request->getRequestUri()); - $siteIdArr = [ - 'id' => $request->input('vid') ?? '0', - 'tvdb' => $request->input('tvdbid') ?? '0', - 'trakt' => $request->input('traktid') ?? '0', - 'tvrage' => $request->input('rid') ?? '0', - 'tvmaze' => $request->input('tvmazeid') ?? '0', - 'imdb' => Str::replace('tt', '', $request->input('imdbid')) ?? '0', - 'tmdb' => $request->input('tmdbid') ?? '0', - ]; + $siteIdArr = [ + 'id' => $request->input('vid') ?? '0', + 'tvdb' => $request->input('tvdbid') ?? '0', + 'trakt' => $request->input('traktid') ?? '0', + 'tvrage' => $request->input('rid') ?? '0', + 'tvmaze' => $request->input('tvmazeid') ?? '0', + 'imdb' => Str::replace('tt', '', $request->input('imdbid')) ?? '0', + 'tmdb' => $request->input('tmdbid') ?? '0', + ]; - // Process season only queries or Season and Episode/Airdate queries + // Process season only queries or Season and Episode/Airdate queries - $series = $request->input('season') ?? ''; - $episode = $request->input('ep') ?? ''; + $series = $request->input('season') ?? ''; + $episode = $request->input('ep') ?? ''; - if (preg_match('#^(19|20)\d{2}$#', $series, $year) && str_contains($episode, '/')) { - $airDate = str_replace('/', '-', $year[0].'-'.$episode); - } + if (preg_match('#^(19|20)\d{2}$#', $series, $year) && str_contains($episode, '/')) { + $airDate = str_replace('/', '-', $year[0].'-'.$episode); + } - $relData = $releases->apiTvSearch( - $siteIdArr, - $series, - $episode, - $airDate ?? '', - $this->offset($request), - $this->limit($request), - $request->input('q') ?? '', - $this->categoryID($request), - $maxAge, - $minSize, - $catExclusions - ); + $relData = $releases->tvSearch( + $siteIdArr, + $series, + $episode, + $airDate ?? '', + $this->offset($request), + $this->limit($request), + $request->input('q') ?? '', + $this->categoryID($request), + $maxAge, + $minSize, + $catExclusions + ); - $this->output($relData, $params, $outputXML, $offset, 'api'); - break; + $this->output($relData, $params, $outputXML, $offset, 'api'); + break; // Search movie releases. - case 'm': - $this->verifyEmptyParameter($request, 'q'); - $this->verifyEmptyParameter($request, 'imdbid'); - $maxAge = $this->maxAge($request); - UserRequest::addApiRequest($apiKey, $request->getRequestUri()); + case 'm': + $this->verifyEmptyParameter($request, 'q'); + $this->verifyEmptyParameter($request, 'imdbid'); + $maxAge = $this->maxAge($request); + UserRequest::addApiRequest($apiKey, $request->getRequestUri()); - $imdbId = $request->has('imdbid') && $request->filled('imdbid') ? (int) $request->input('imdbid') : -1; - $tmdbId = $request->has('tmdbid') && $request->filled('tmdbid') ? (int) $request->input('tmdbid') : -1; - $traktId = $request->has('traktid') && $request->filled('traktid') ? (int) $request->input('traktid') : -1; + $imdbId = $request->has('imdbid') && $request->filled('imdbid') ? (int) $request->input('imdbid') : -1; + $tmdbId = $request->has('tmdbid') && $request->filled('tmdbid') ? (int) $request->input('tmdbid') : -1; + $traktId = $request->has('traktid') && $request->filled('traktid') ? (int) $request->input('traktid') : -1; - $relData = $releases->moviesSearch( - $imdbId, - $tmdbId, - $traktId, - $this->offset($request), - $this->limit($request), - $request->input('q') ?? '', - $this->categoryID($request), - $maxAge, - $minSize, - $catExclusions - ); + $relData = $releases->moviesSearch( + $imdbId, + $tmdbId, + $traktId, + $this->offset($request), + $this->limit($request), + $request->input('q') ?? '', + $this->categoryID($request), + $maxAge, + $minSize, + $catExclusions + ); - $this->addCoverURL( - $relData, - function ($release) { - return Utility::getCoverURL(['type' => 'movies', 'id' => $release->imdbid]); - } - ); + $this->addCoverURL( + $relData, + function ($release) { + return Utility::getCoverURL(['type' => 'movies', 'id' => $release->imdbid]); + } + ); - $this->output($relData, $params, $outputXML, $offset, 'api'); - break; + $this->output($relData, $params, $outputXML, $offset, 'api'); + break; // Get NZB. - case 'g': - $this->verifyEmptyParameter($request, 'g'); - UserRequest::addApiRequest($apiKey, $request->getRequestUri()); - $relData = Release::checkGuidForApi($request->input('id')); - if ($relData) { - return redirect(url('/getnzb?r='.$apiKey.'&id='.$request->input('id').(($request->has('del') && $request->input('del') === '1') ? '&del=1' : ''))); - } + case 'g': + $this->verifyEmptyParameter($request, 'g'); + UserRequest::addApiRequest($apiKey, $request->getRequestUri()); + $relData = Release::checkGuidForApi($request->input('id')); + if ($relData) { + return redirect(url('/getnzb?r='.$apiKey.'&id='.$request->input('id').(($request->has('del') && $request->input('del') === '1') ? '&del=1' : ''))); + } - return Utility::showApiError(300, 'No such item (the guid you provided has no release in our database)'); + return Utility::showApiError(300, 'No such item (the guid you provided has no release in our database)'); // Get individual NZB details. - case 'd': - if ($request->missing('id')) { - return Utility::showApiError(200, 'Missing parameter (guid is required for single release details)'); - } + case 'd': + if ($request->missing('id')) { + return Utility::showApiError(200, 'Missing parameter (guid is required for single release details)'); + } - UserRequest::addApiRequest($apiKey, $request->getRequestUri()); - $data = Release::getByGuid($request->input('id')); + UserRequest::addApiRequest($apiKey, $request->getRequestUri()); + $data = Release::getByGuid($request->input('id')); - $this->output($data, $params, $outputXML, $offset, 'api'); - break; + $this->output($data, $params, $outputXML, $offset, 'api'); + break; // Get an NFO file for an individual release. - case 'n': - if ($request->missing('id')) { - return Utility::showApiError(200, 'Missing parameter (id is required for retrieving an NFO)'); - } - - UserRequest::addApiRequest($apiKey, $request->getRequestUri()); - $rel = Release::query()->where('guid', $request->input('id'))->first(['id', 'searchname']); - - if ($rel && $rel->isNotEmpty()) { - $data = ReleaseNfo::getReleaseNfo($rel->id); - if (! empty($data)) { - if ($request->has('o') && $request->input('o') === 'file') { - return response()->streamDownload(function () use ($data) { - echo $data['nfo']; - }, $rel['searchname'].'.nfo', ['Content-type:' => 'application/octet-stream']); - } - - echo nl2br(Utility::cp437toUTF($data['nfo'])); - } else { - return Utility::showApiError(300, 'Release does not have an NFO file associated.'); + case 'n': + if ($request->missing('id')) { + return Utility::showApiError(200, 'Missing parameter (id is required for retrieving an NFO)'); } - } else { - return Utility::showApiError(300, 'Release does not exist.'); - } - break; + + UserRequest::addApiRequest($apiKey, $request->getRequestUri()); + $rel = Release::query()->where('guid', $request->input('id'))->first(['id', 'searchname']); + + if ($rel && $rel->isNotEmpty()) { + $data = ReleaseNfo::getReleaseNfo($rel->id); + if (! empty($data)) { + if ($request->has('o') && $request->input('o') === 'file') { + return response()->streamDownload(function () use ($data) { + echo $data['nfo']; + }, $rel['searchname'].'.nfo', ['Content-type:' => 'application/octet-stream']); + } + + echo nl2br(Utility::cp437toUTF($data['nfo'])); + } else { + return Utility::showApiError(300, 'Release does not have an NFO file associated.'); + } + } else { + return Utility::showApiError(300, 'Release does not exist.'); + } + break; // Capabilities request. - case 'c': - $this->output([], $params, $outputXML, $offset, 'caps'); - break; + case 'c': + $this->output([], $params, $outputXML, $offset, 'caps'); + break; + } } - } - /** - * @throws \Exception - */ - public function output($data, array $params, bool $xml, int $offset, string $type = '') - { - $this->type = $type; - $options = [ - 'Parameters' => $params, - 'Data' => $data, - 'Server' => $this->getForMenu(), - 'Offset' => $offset, - 'Type' => $type, - ]; + /** + * @throws \Exception + */ + public function output($data, array $params, bool $xml, int $offset, string $type = '') + { + $this->type = $type; + $options = [ + 'Parameters' => $params, + 'Data' => $data, + 'Server' => $this->getForMenu(), + 'Offset' => $offset, + 'Type' => $type, + ]; - // Generate the XML Response - $response = (new XML_Response($options))->returnXML(); + // Generate the XML Response + $response = (new XML_Response($options))->returnXML(); - if ($xml) { - header('Content-type: text/xml'); - } else { - // JSON encode the XMLWriter response - $response = json_encode(xml_to_array($response), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES); - header('Content-type: application/json'); - } - if ($response === false) { - return Utility::showApiError(201); - } else { - header('Content-Length: '.\strlen($response)); - echo $response; - exit; - } - } - - /** - * Collect and return various capability information for usage in API. - * - * - * @throws \Exception - */ - public function getForMenu(): array - { - $serverroot = url('/'); - - return [ - 'server' => [ - 'title' => config('app.name'), - 'strapline' => Settings::settingValue('strapline'), - 'email' => config('mail.from.address'), - 'meta' => Settings::settingValue('metakeywords'), - 'url' => $serverroot, - 'image' => $serverroot.'/assets/images/tmux_logo.png', - ], - 'limits' => [ - 'max' => 100, - 'default' => 100, - ], - 'registration' => [ - 'available' => 'yes', - 'open' => (int) Settings::settingValue('registerstatus') === 0 ? 'yes' : 'no', - ], - 'searching' => [ - 'search' => ['available' => 'yes', 'supportedParams' => 'q'], - 'tv-search' => ['available' => 'yes', 'supportedParams' => 'q,vid,tvdbid,traktid,rid,tvmazeid,imdbid,tmdbid,season,ep'], - 'movie-search' => ['available' => 'yes', 'supportedParams' => 'q,imdbid, tmdbid, traktid'], - 'audio-search' => ['available' => 'no', 'supportedParams' => ''], - ], - 'categories' => $this->type === 'caps' - ? Category::getForMenu() - : null, - ]; - } - - /** - * @return Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Foundation\Application|\Illuminate\Http\Response|int - */ - public function maxAge(Request $request) - { - $maxAge = -1; - if ($request->has('maxage')) { - if (! $request->filled('maxage')) { - return Utility::showApiError(201, 'Incorrect parameter (maxage must not be empty)'); - } elseif (! is_numeric($request->input('maxage'))) { - return Utility::showApiError(201, 'Incorrect parameter (maxage must be numeric)'); + if ($xml) { + header('Content-type: text/xml'); } else { - $maxAge = (int) $request->input('maxage'); + // JSON encode the XMLWriter response + $response = json_encode(xml_to_array($response), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES); + header('Content-type: application/json'); + } + if ($response === false) { + return Utility::showApiError(201); + } else { + header('Content-Length: '.\strlen($response)); + echo $response; + exit; } } - return $maxAge; - } + /** + * Collect and return various capability information for usage in API. + * + * + * @throws \Exception + */ + public function getForMenu(): array + { + $serverroot = url('/'); - /** - * Verify cat parameter. - */ - public function categoryID(Request $request): array - { - $categoryID[] = -1; - if ($request->has('cat')) { - $categoryIDs = urldecode($request->input('cat')); - // Append Web-DL category ID if HD present for SickBeard / Sonarr compatibility. - if (str_contains($categoryIDs, (string) Category::TV_HD) && ! str_contains($categoryIDs, (string) Category::TV_WEBDL) && (int) Settings::settingValue('catwebdl') === 0) { - $categoryIDs .= (','.Category::TV_WEBDL); - } - $categoryID = explode(',', $categoryIDs); + return [ + 'server' => [ + 'title' => config('app.name'), + 'strapline' => Settings::settingValue('strapline'), + 'email' => config('mail.from.address'), + 'meta' => Settings::settingValue('metakeywords'), + 'url' => $serverroot, + 'image' => $serverroot.'/assets/images/tmux_logo.png', + ], + 'limits' => [ + 'max' => 100, + 'default' => 100, + ], + 'registration' => [ + 'available' => 'yes', + 'open' => (int) Settings::settingValue('registerstatus') === 0 ? 'yes' : 'no', + ], + 'searching' => [ + 'search' => ['available' => 'yes', 'supportedParams' => 'q'], + 'tv-search' => ['available' => 'yes', 'supportedParams' => 'q,vid,tvdbid,traktid,rid,tvmazeid,imdbid,tmdbid,season,ep'], + 'movie-search' => ['available' => 'yes', 'supportedParams' => 'q,imdbid, tmdbid, traktid'], + 'audio-search' => ['available' => 'no', 'supportedParams' => ''], + ], + 'categories' => $this->type === 'caps' + ? Category::getForMenu() + : null, + ]; } - return $categoryID; - } + /** + * @return Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Foundation\Application|\Illuminate\Http\Response|int + */ + public function maxAge(Request $request) + { + $maxAge = -1; + if ($request->has('maxage')) { + if (! $request->filled('maxage')) { + return Utility::showApiError(201, 'Incorrect parameter (maxage must not be empty)'); + } elseif (! is_numeric($request->input('maxage'))) { + return Utility::showApiError(201, 'Incorrect parameter (maxage must be numeric)'); + } else { + $maxAge = (int) $request->input('maxage'); + } + } - /** - * Verify groupName parameter. - * - * - * @throws \Exception - */ - public function group(Request $request): string|int|bool - { - $groupName = -1; - if ($request->has('group')) { - $group = UsenetGroup::isValidGroup($request->input('group')); - if ($group !== false) { - $groupName = $group; + return $maxAge; + } + + /** + * Verify cat parameter. + */ + public function categoryID(Request $request): array + { + $categoryID[] = -1; + if ($request->has('cat')) { + $categoryIDs = urldecode($request->input('cat')); + // Append Web-DL category ID if HD present for SickBeard / Sonarr compatibility. + if (str_contains($categoryIDs, (string) Category::TV_HD) && ! str_contains($categoryIDs, (string) Category::TV_WEBDL) && (int) Settings::settingValue('catwebdl') === 0) { + $categoryIDs .= (','.Category::TV_WEBDL); + } + $categoryID = explode(',', $categoryIDs); + } + + return $categoryID; + } + + /** + * Verify groupName parameter. + * + * + * @throws \Exception + */ + public function group(Request $request): string|int|bool + { + $groupName = -1; + if ($request->has('group')) { + $group = UsenetGroup::isValidGroup($request->input('group')); + if ($group !== false) { + $groupName = $group; + } + } + + return $groupName; + } + + /** + * Verify limit parameter. + */ + public function limit(Request $request): int + { + $limit = 100; + if ($request->has('limit') && is_numeric($request->input('limit'))) { + $limit = (int) $request->input('limit'); + } + + return $limit; + } + + /** + * Verify offset parameter. + */ + public function offset(Request $request): int + { + $offset = 0; + if ($request->has('offset') && is_numeric($request->input('offset'))) { + $offset = (int) $request->input('offset'); + } + + return $offset; + } + + /** + * Check if a parameter is empty. + */ + public function verifyEmptyParameter(Request $request, string $parameter) + { + if ($request->has($parameter) && $request->isNotFilled($parameter)) { + return Utility::showApiError(201, 'Incorrect parameter ('.$parameter.' must not be empty)'); } } - return $groupName; - } - - /** - * Verify limit parameter. - */ - public function limit(Request $request): int - { - $limit = 100; - if ($request->has('limit') && is_numeric($request->input('limit'))) { - $limit = (int) $request->input('limit'); - } - - return $limit; - } - - /** - * Verify offset parameter. - */ - public function offset(Request $request): int - { - $offset = 0; - if ($request->has('offset') && is_numeric($request->input('offset'))) { - $offset = (int) $request->input('offset'); - } - - return $offset; - } - - /** - * Check if a parameter is empty. - */ - public function verifyEmptyParameter(Request $request, string $parameter) - { - if ($request->has($parameter) && $request->isNotFilled($parameter)) { - return Utility::showApiError(201, 'Incorrect parameter ('.$parameter.' must not be empty)'); - } - } - - public function addCoverURL(&$releases, callable $getCoverURL): void - { - if ($releases && \count($releases)) { - foreach ($releases as $key => $release) { - if (isset($release->id)) { - $release->coverurl = $getCoverURL($release); + public function addCoverURL(&$releases, callable $getCoverURL): void + { + if ($releases && \count($releases)) { + foreach ($releases as $key => $release) { + if (isset($release->id)) { + $release->coverurl = $getCoverURL($release); + } } } } } -} diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index 64fdc31f4..a4b2c30da 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -1,301 +1,697 @@ . + * + * @author ruhllatio + * @copyright 2016 nZEDb + */ -use App\Events\UserAccessedApi; -use App\Http\Controllers\BasePageController; -use App\Models\Category; -use App\Models\Release; -use App\Models\Settings; -use App\Models\User; -use App\Models\UserDownload; -use App\Models\UserRequest; -use App\Transformers\ApiTransformer; -use App\Transformers\CategoryTransformer; -use App\Transformers\DetailsTransformer; -use Blacklight\Releases; -use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; -use Illuminate\Support\Carbon; + namespace App\Http\Controllers\Api; -class ApiV2Controller extends BasePageController -{ - private ApiController $api; - - public function __construct() - { - $this->api = new ApiController; - } - - public function capabilities(): JsonResponse - { - $category = Category::getForApi(); - - $capabilities = [ - 'server' => [ - 'title' => config('app.name'), - 'strapline' => Settings::settingValue('strapline'), - 'email' => config('mail.from.address'), - 'url' => url('/'), - ], - 'limits' => [ - 'max' => 100, - 'default' => 100, - ], - 'registration' => [ - 'available' => 'no', - 'open' => (int) Settings::settingValue('registerstatus') === 0 ? 'yes' : 'no', - ], - 'searching' => [ - 'search' => ['available' => 'yes', 'supportedParams' => 'id'], - 'tv-search' => ['available' => 'yes', 'supportedParams' => 'id,vid,tvdbid,traktid,rid,tvmazeid,imdbid,tmdbid,season,ep'], - 'movie-search' => ['available' => 'yes', 'supportedParams' => 'id, imdbid, tmdbid, traktid'], - 'audio-search' => ['available' => 'no', 'supportedParams' => ''], - ], - 'categories' => fractal($category, new CategoryTransformer), - ]; - - return response()->json($capabilities); - } + use App\Models\Category; + use App\Models\Release; + use Illuminate\Support\Carbon; /** - * @throws \Throwable + * Class XMLReturn. */ - public function movie(Request $request): JsonResponse + class XML_Response { - if ($request->missing('api_token') || ($request->has('api_token') && $request->isNotFilled('api_token'))) { - return response()->json(['error' => 'Missing parameter (apikey)'], 403); + /** + * @var string The buffered cData before final write + */ + protected string $cdata; + + /** + * The RSS namespace used for the output. + */ + protected string $namespace; + + /** + * The trailing URL parameters on the request. + */ + protected mixed $parameters; + + /** + * The release we are adding to the stream. + */ + protected mixed $release; + + /** + * The retrieved releases we are returning from the API call. + */ + protected mixed $releases; + + /** + * The various server variables and active categories. + */ + protected mixed $server; + + /** + * The XML formatting operation we are returning. + */ + protected mixed $type; + + /** + * The XMLWriter Class. + */ + protected \XMLWriter $xml; + + protected mixed $offset; + + /** + * XMLReturn constructor. + */ + public function __construct(array $options = []) + { + $defaults = [ + 'Parameters' => null, + 'Data' => null, + 'Server' => null, + 'Offset' => null, + 'Type' => null, + ]; + $options += $defaults; + + $this->parameters = $options['Parameters']; + $this->releases = $options['Data']; + $this->server = $options['Server']; + $this->offset = $options['Offset']; + $this->type = $options['Type']; + + $this->xml = new \XMLWriter; + $this->xml->openMemory(); + $this->xml->setIndent(true); } - $releases = new Releases; - $user = User::query()->where('api_token', $request->input('api_token'))->first(); - $minSize = $request->has('minsize') && $request->input('minsize') > 0 ? $request->input('minsize') : 0; - $maxAge = $this->api->maxAge($request); - $catExclusions = User::getCategoryExclusionForApi($request); - UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri()); - event(new UserAccessedApi($user)); - $imdbId = $request->has('imdbid') && $request->filled('imdbid') ? $request->input('imdbid') : -1; - $tmdbId = $request->has('tmdbid') && $request->filled('tmdbid') ? $request->input('tmdbid') : -1; - $traktId = $request->has('traktid') && $request->filled('traktid') ? $request->input('traktid') : -1; + public function returnXML(): bool|string + { + if ($this->xml) { + switch ($this->type) { + case 'caps': + return $this->returnCaps(); + break; + case 'api': + $this->namespace = 'newznab'; - $relData = $releases->moviesSearch( - $imdbId, - $tmdbId, - $traktId, - $this->api->offset($request), - $this->api->limit($request), - $request->input('id') ?? '', - $this->api->categoryID($request), - $maxAge, - $minSize, - $catExclusions - ); + return $this->returnApiXml(); + break; + case 'rss': + $this->namespace = 'nntmux'; - $time = UserRequest::whereUsersId($user->id)->min('timestamp'); - $apiOldestTime = $time !== null ? Carbon::createFromTimeString($time)->toRfc2822String() : ''; - $grabTime = UserDownload::whereUsersId($user->id)->min('timestamp'); - $oldestGrabTime = $grabTime !== null ? Carbon::createFromTimeString($grabTime)->toRfc2822String() : ''; + return $this->returnApiRssXml(); + break; + case 'reg': + return $this->returnReg(); + break; + } + } - $response = [ - 'Total' => $relData[0]->_totalrows ?? 0, - 'apiCurrent' => UserRequest::getApiRequests($user->id), - 'apiMax' => $user->role->apirequests, - 'grabCurrent' => UserDownload::getDownloadRequests($user->id), - 'grabMax' => $user->role->downloadrequests, - 'apiOldestTime' => $apiOldestTime, - 'grabOldestTime' => $oldestGrabTime, - 'Results' => fractal($relData, new ApiTransformer($user)), - ]; - - return response()->json($response); - } - - /** - * @throws \Exception - * @throws \Throwable - */ - public function apiSearch(Request $request): JsonResponse - { - if ($request->missing('api_token') || $request->isNotFilled('api_token')) { - return response()->json(['error' => 'Missing parameter (api_token)'], 403); + return false; } - $releases = new Releases; - $user = User::query()->where('api_token', $request->input('api_token'))->first(); - $offset = $this->api->offset($request); - $catExclusions = User::getCategoryExclusionForApi($request); - $minSize = $request->has('minsize') && $request->input('minsize') > 0 ? $request->input('minsize') : 0; - $maxAge = $this->api->maxAge($request); - $groupName = $this->api->group($request); - UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri()); - event(new UserAccessedApi($user)); - $categoryID = $this->api->categoryID($request); - $limit = $this->api->limit($request); - if ($request->has('id')) { - $relData = $releases->apiSearch( - $request->input('id'), - $groupName, - $offset, - $limit, - $maxAge, - $catExclusions, - $categoryID, - $minSize + /** + * XML writes and returns the API capabilities. + * + * @return string The XML Formatted string data + */ + protected function returnCaps(): string + { + $this->xml->startDocument('1.0', 'UTF-8'); + $this->xml->startElement('caps'); + $this->addNode(['name' => 'server', 'data' => $this->server['server']]); + $this->addNode(['name' => 'limits', 'data' => $this->server['limits']]); + $this->addNode(['name' => 'registration', 'data' => $this->server['registration']]); + $this->addNodes(['name' => 'searching', 'data' => $this->server['searching']]); + $this->writeCategoryListing(); + $this->xml->endElement(); + $this->xml->endDocument(); + + return $this->xml->outputMemory(); + } + + /** + * XML writes and returns the API data. + * + * @return string The XML Formatted string data + */ + protected function returnApiRssXml(): string + { + $this->xml->startDocument('1.0', 'UTF-8'); + $this->includeRssAtom(); // Open RSS + $this->xml->startElement('channel'); // Open channel + $this->includeRssAtomLink(); + $this->includeMetaInfo(); + $this->includeImage(); + $this->includeTotalRows(); + $this->includeLimits(); + $this->includeReleases(); + $this->xml->endElement(); // End channel + $this->xml->endElement(); // End RSS + $this->xml->endDocument(); + + return $this->xml->outputMemory(); + } + + /** + * XML writes and returns the API data. + * + * @return string The XML Formatted string data + */ + protected function returnApiXml(): string + { + $this->xml->startDocument('1.0', 'UTF-8'); + $this->includeRssAtom(); // Open RSS + $this->xml->startElement('channel'); // Open channel + $this->includeMetaInfo(); + $this->includeImage(); + $this->includeTotalRows(); + $this->includeLimits(); + $this->includeReleases(); + $this->xml->endElement(); // End channel + $this->xml->endElement(); // End RSS + $this->xml->endDocument(); + + return $this->xml->outputMemory(); + } + + /** + * @return string The XML formatted registration information + */ + protected function returnReg(): string + { + $this->xml->startDocument('1.0', 'UTF-8'); + $this->xml->startElement('register'); + $this->xml->writeAttribute('username', $this->parameters['username']); + $this->xml->writeAttribute('password', $this->parameters['password']); + $this->xml->writeAttribute('apikey', $this->parameters['token']); + $this->xml->endElement(); + $this->xml->endDocument(); + + return $this->xml->outputMemory(); + } + + /** + * Starts a new element, loops through the attribute data and ends the element. + * + * @param array $element An array with the name of the element and the attribute data + */ + protected function addNode(array $element): void + { + $this->xml->startElement($element['name']); + foreach ($element['data'] as $attr => $val) { + $this->xml->writeAttribute($attr, $val); + } + $this->xml->endElement(); + } + + /** + * Starts a new element, loops through the attribute data and ends the element. + * + * @param array $element An array with the name of the element and the attribute data + */ + protected function addNodes(array $element): void + { + $this->xml->startElement($element['name']); + foreach ($element['data'] as $elem => $value) { + $subelement['name'] = $elem; + $subelement['data'] = $value; + $this->addNode($subelement); + } + $this->xml->endElement(); + } + + /** + * Adds the site category listing to the XML feed. + */ + protected function writeCategoryListing(): void + { + $this->xml->startElement('categories'); + foreach ($this->server['categories'] as $this->parameters) { + $this->xml->startElement('category'); + $this->xml->writeAttribute('id', $this->parameters['id']); + $this->xml->writeAttribute('name', html_entity_decode($this->parameters['title'])); + if (! empty($this->parameters['description'])) { + $this->xml->writeAttribute('description', html_entity_decode($this->parameters['description'])); + } + foreach ($this->parameters['categories'] as $c) { + $this->xml->startElement('subcat'); + $this->xml->writeAttribute('id', $c['id']); + $this->xml->writeAttribute('name', html_entity_decode($c['title'])); + if (! empty($c['description'])) { + $this->xml->writeAttribute('description', html_entity_decode($c['description'])); + } + $this->xml->endElement(); + } + $this->xml->endElement(); + } + } + + /** + * Adds RSS Atom information to the XML. + */ + protected function includeRssAtom(): void + { + $url = match ($this->namespace) { + 'newznab' => 'http://www.newznab.com/DTD/2010/feeds/attributes/', + default => $this->server['server']['url'].'/rss-info/', + }; + + $this->xml->startElement('rss'); + $this->xml->writeAttribute('version', '2.0'); + $this->xml->writeAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom'); + $this->xml->writeAttribute("xmlns:{$this->namespace}", $url); + $this->xml->writeAttribute('encoding', 'utf-8'); + } + + protected function includeRssAtomLink(): void + { + $this->xml->startElement('atom:link'); + $this->xml->startAttribute('href'); + $this->xml->text($this->server['server']['url'].($this->namespace === 'newznab' ? '/api/v1/api' : '/rss')); + $this->xml->endAttribute(); + $this->xml->startAttribute('rel'); + $this->xml->text('self'); + $this->xml->endAttribute(); + $this->xml->startAttribute('type'); + $this->xml->text('application/rss+xml'); + $this->xml->endAttribute(); + $this->xml->endElement(); + } + + /** + * Writes the channel information for the feed. + */ + protected function includeMetaInfo(): void + { + $server = $this->server['server']; + + switch ($this->namespace) { + case 'newznab': + $path = '/apihelp/'; + $tag = 'API'; + break; + case 'nntmux': + default: + $path = '/rss-info/'; + $tag = 'RSS'; + } + + $this->xml->writeElement('title', $server['title']); + $this->xml->writeElement('description', $server['title']." {$tag} Details"); + $this->xml->writeElement('link', $server['url']); + $this->xml->writeElement('language', 'en-gb'); + $this->xml->writeElement('webMaster', $server['email'].' '.$server['title']); + $this->xml->writeElement('category', $server['meta']); + $this->xml->writeElement('generator', 'nntmux'); + $this->xml->writeElement('ttl', '10'); + $this->xml->writeElement('docs', $this->server['server']['url'].$path); + } + + /** + * Adds nntmux logo data to the XML. + */ + protected function includeImage(): void + { + $this->xml->startElement('image'); + $this->xml->writeAttribute('url', $this->server['server']['url'].'/assets/images/tmux_logo.png'); + $this->xml->writeAttribute('title', $this->server['server']['title']); + $this->xml->writeAttribute('link', $this->server['server']['url']); + $this->xml->writeAttribute( + 'description', + 'Visit '.$this->server['server']['title'].' - '.$this->server['server']['strapline'] ); - } else { - $relData = $releases->getBrowseRange( - 1, - $categoryID, - $offset, - $limit, - '', - $maxAge, - $catExclusions, - $groupName, - $minSize + $this->xml->endElement(); + } + + public function includeTotalRows(): void + { + $this->xml->startElement($this->namespace.':response'); + $this->xml->writeAttribute('offset', $this->offset); + $this->xml->writeAttribute('total', $this->releases[0]->_totalrows ?? 0); + $this->xml->endElement(); + } + + public function includeLimits(): void + { + $this->xml->startElement($this->namespace.':apilimits'); + $this->xml->writeAttribute('apicurrent', $this->parameters['requests']); + $this->xml->writeAttribute('apimax', $this->parameters['apilimit']); + $this->xml->writeAttribute('grabcurrent', $this->parameters['grabs']); + $this->xml->writeAttribute('grabmax', $this->parameters['downloadlimit']); + if (! empty($this->parameters['oldestapi'])) { + $this->xml->writeAttribute('apioldesttime', $this->parameters['oldestapi']); + } + if (! empty($this->parameters['oldestgrab'])) { + $this->xml->writeAttribute('graboldesttime', $this->parameters['oldestgrab']); + } + $this->xml->endElement(); + } + + /** + * Loop through the releases and add their info to the XML stream. + */ + public function includeReleases(): void + { + if (! empty($this->releases)) { + if (! $this->releases instanceof Release) { + foreach ($this->releases as $this->release) { + $this->xml->startElement('item'); + $this->includeReleaseMain(); + $this->setZedAttributes(); + $this->xml->endElement(); + } + } else { + $this->release = $this->releases; + $this->xml->startElement('item'); + $this->includeReleaseMain(); + $this->setZedAttributes(); + $this->xml->endElement(); + } + } + } + + /** + * Writes the primary release information. + */ + public function includeReleaseMain(): void + { + $this->xml->writeElement('title', $this->release->searchname); + $this->xml->startElement('guid'); + $this->xml->writeAttribute('isPermaLink', 'true'); + $this->xml->text("{$this->server['server']['url']}/details/{$this->release->guid}"); + $this->xml->endElement(); + $this->xml->writeElement( + 'link', + "{$this->server['server']['url']}/getnzb?id={$this->release->guid}.nzb". + "&r={$this->parameters['token']}". + ((int) $this->parameters['del'] === 1 ? '&del=1' : '') ); + $this->xml->writeElement('comments', "{$this->server['server']['url']}/details/{$this->release->guid}#comments"); + $this->xml->writeElement('pubDate', date(DATE_RSS, strtotime($this->release->adddate))); + $this->xml->writeElement('category', $this->release->category_name); + if ($this->namespace === 'newznab') { + $this->xml->writeElement('description', $this->release->searchname); + } else { + $this->writeRssCdata(); + } + if (! isset($this->parameters['dl']) || (isset($this->parameters['dl']) && (int) $this->parameters['dl'] === 1)) { + $this->xml->startElement('enclosure'); + $this->xml->writeAttribute( + 'url', + "{$this->server['server']['url']}/getnzb?id={$this->release->guid}.nzb". + "&r={$this->parameters['token']}". + ((int) $this->parameters['del'] === 1 ? '&del=1' : '') + ); + $this->xml->writeAttribute('length', $this->release->size); + $this->xml->writeAttribute('type', 'application/x-nzb'); + $this->xml->endElement(); + } } - $time = UserRequest::whereUsersId($user->id)->min('timestamp'); - $apiOldestTime = $time !== null ? Carbon::createFromTimeString($time)->toRfc2822String() : ''; - $grabTime = UserDownload::whereUsersId($user->id)->min('timestamp'); - $oldestGrabTime = $grabTime !== null ? Carbon::createFromTimeString($grabTime)->toRfc2822String() : ''; + /** + * Writes the Zed (newznab) specific attributes. + */ + protected function setZedAttributes(): void + { + $this->writeZedAttr('category', $this->release->categories_id); + $this->writeZedAttr('size', $this->release->size); + if (! empty($this->release->coverurl)) { + $this->writeZedAttr( + 'coverurl', + $this->server['server']['url']."/covers/{$this->release->coverurl}" + ); + } - $response = [ - 'Total' => $relData[0]->_totalrows ?? 0, - 'apiCurrent' => UserRequest::getApiRequests($user->id), - 'apiMax' => $user->role->apirequests, - 'grabCurrent' => UserDownload::getDownloadRequests($user->id), - 'grabMax' => $user->role->downloadrequests, - 'apiOldestTime' => $apiOldestTime, - 'grabOldestTime' => $oldestGrabTime, - 'Results' => fractal($relData, new ApiTransformer($user)), - ]; + if ((int) $this->parameters['extended'] === 1) { + $this->writeZedAttr('files', $this->release->totalpart); + $this->writeZedAttr('poster', $this->release->fromname); + if (($this->release->videos_id > 0 || $this->release->tv_episodes_id > 0) && $this->namespace === 'newznab') { + $this->setTvAttr(); + } - return response()->json($response); + if (isset($this->release->imdbid) && $this->release->imdbid > 0) { + $this->writeZedAttr('imdb', $this->release->imdbid); + } + if (isset($this->release->anidbid) && $this->release->anidbid > 0) { + $this->writeZedAttr('anidbid', $this->release->anidbid); + } + if (isset($this->release->predb_id) && $this->release->predb_id > 0) { + $this->writeZedAttr('prematch', 1); + } + if (isset($this->release->nfostatus) && (int) $this->release->nfostatus === 1) { + $this->writeZedAttr( + 'info', + $this->server['server']['url']. + "api?t=info&id={$this->release->guid}&r={$this->parameters['token']}" + ); + } + + $this->writeZedAttr('grabs', $this->release->grabs); + $this->writeZedAttr('comments', $this->release->comments); + $this->writeZedAttr('password', $this->release->passwordstatus); + $this->writeZedAttr('usenetdate', Carbon::parse($this->release->postdate)->toRssString()); + if (! empty($this->release->group_name)) { + $this->writeZedAttr('group', $this->release->group_name); + } + } + } + + /** + * Writes the TV Specific attributes. + */ + protected function setTvAttr(): void + { + if (! empty($this->release->title)) { + $this->writeZedAttr('title', $this->release->title); + } + if (isset($this->release->series) && $this->release->series > 0) { + $this->writeZedAttr('season', $this->release->series); + } + if (isset($this->release->episode->episode) && $this->release->episode->episode > 0) { + $this->writeZedAttr('episode', $this->release->episode->episode); + } + if (! empty($this->release->firstaired)) { + $this->writeZedAttr('tvairdate', $this->release->firstaired); + } + if (isset($this->release->tvdb) && $this->release->tvdb > 0) { + $this->writeZedAttr('tvdbid', $this->release->tvdb); + } + if (isset($this->release->trakt) && $this->release->trakt > 0) { + $this->writeZedAttr('traktid', $this->release->trakt); + } + if (isset($this->release->tvrage) && $this->release->tvrage > 0) { + $this->writeZedAttr('tvrageid', $this->release->tvrage); + $this->writeZedAttr('rageid', $this->release->tvrage); + } + if (isset($this->release->tvmaze) && $this->release->tvmaze > 0) { + $this->writeZedAttr('tvmazeid', $this->release->tvmaze); + } + if (isset($this->release->imdb) && $this->release->imdb > 0) { + $this->writeZedAttr('imdbid', $this->release->imdb); + } + if (isset($this->release->tmdb) && $this->release->tmdb > 0) { + $this->writeZedAttr('tmdbid', $this->release->tmdb); + } + } + + /** + * Writes individual zed (newznab) type attributes. + * + * @param string $name The namespaced attribute name tag + * @param string $value The namespaced attribute value + */ + protected function writeZedAttr(string $name, string $value): void + { + $this->xml->startElement($this->namespace.':attr'); + $this->xml->writeAttribute('name', $name); + $this->xml->writeAttribute('value', $value); + $this->xml->endElement(); + } + + /** + * Writes the cData (HTML format) for the RSS feed + * Also calls supplementary cData writes depending upon post process. + */ + protected function writeRssCdata(): void + { + $this->cdata = "\n\t
\n"; + switch (1) { + case ! empty($this->release->cover): + $dir = 'movies'; + $column = 'imdbid'; + break; + case ! empty($this->release->mu_cover): + $dir = 'music'; + $column = 'musicinfo_id'; + break; + case ! empty($this->release->co_cover): + $dir = 'console'; + $column = 'consoleinfo_id'; + break; + case ! empty($this->release->bo_cover): + $dir = 'books'; + $column = 'bookinfo_id'; + break; + } + if (isset($dir, $column)) { + $dcov = ($dir === 'movies' ? '-cover' : ''); + $this->cdata .= + "\tserver['server']['url']}/covers/{$dir}/{$this->release->$column}{$dcov}.jpg\" ". + "width=\"120\" alt=\"{$this->release->searchname}\" />\n"; + } + $size = human_filesize($this->release->size); + $this->cdata .= + "\t
  • ID: server['server']['url']}/details/{$this->release->guid}\">{$this->release->guid}
  • \n". + "\t
  • Name: {$this->release->searchname}
  • \n". + "\t
  • Size: {$size}
  • \n". + "\t
  • Category: server['server']['url']}/browse/{$this->release->category_name}\">{$this->release->category_name}
  • \n". + "\t
  • Group: server['server']['url']}/browse/group?g={$this->release->group_name}\">{$this->release->group_name}
  • \n". + "\t
  • Poster: {$this->release->fromname}
  • \n". + "\t
  • Posted: {$this->release->postdate}
  • \n"; + + $pstatus = match ($this->release->passwordstatus) { + 0 => 'None', + 1 => 'Possibly Passworded', + 2 => 'Probably not viable', + 10 => 'Passworded', + default => 'Unknown', + }; + $this->cdata .= "\t
  • Password: {$pstatus}
  • \n"; + if ($this->release->nfostatus === 1) { + $this->cdata .= + "\t
  • Nfo: ". + "server['server']['url']}/api?t=nfo&id={$this->release->guid}&raw=1&i={$this->parameters['uid']}&r={$this->parameters['token']}\">". + "{$this->release->searchname}.nfo
  • \n"; + } + + if ($this->release->parentid === Category::MOVIE_ROOT && $this->release->imdbid !== '') { + $this->writeRssMovieInfo(); + } elseif ($this->release->parentid === Category::MUSIC_ROOT && $this->release->musicinfo_id > 0) { + $this->writeRssMusicInfo(); + } elseif ($this->release->parentid === Category::GAME_ROOT && $this->release->consoleinfo_id > 0) { + $this->writeRssConsoleInfo(); + } + $this->xml->startElement('description'); + $this->xml->writeCdata($this->cdata."\t
    "); + $this->xml->endElement(); + } + + /** + * Writes the Movie Info for the RSS feed cData. + */ + protected function writeRssMovieInfo(): void + { + $movieCol = ['rating', 'plot', 'year', 'genre', 'director', 'actors']; + + $cData = $this->buildCdata($movieCol); + + $this->cdata .= + "\t
  • Imdb Info: + \t + \t
  • + \n"; + } + + /** + * Writes the Music Info for the RSS feed cData. + */ + protected function writeRssMusicInfo(): void + { + $tData = $cDataUrl = ''; + + $musicCol = ['mu_artist', 'mu_genre', 'mu_publisher', 'mu_releasedate', 'mu_review']; + + $cData = $this->buildCdata($musicCol); + + if ($this->release->mu_url !== '') { + $cDataUrl = "
  • Amazon: release->mu_url}\">{$this->release->mu_title}
  • "; + } + + $this->cdata .= + "\t
  • Music Info: + +
  • \n"; + if ($this->release->mu_tracks !== '') { + $tracks = explode('|', $this->release->mu_tracks); + if (\count($tracks) > 0) { + foreach ($tracks as $track) { + $track = trim($track); + $tData .= "
  • {$track}
  • "; + } + } + $this->cdata .= " +
  • Track Listing: +
      + {$tData} +
    +
  • \n"; + } + } + + /** + * Writes the Console Info for the RSS feed cData. + */ + protected function writeRssConsoleInfo(): void + { + $gamesCol = ['co_genre', 'co_publisher', 'year', 'co_review']; + + $cData = $this->buildCdata($gamesCol); + + $this->cdata .= " +
  • Console Info: + +
  • \n"; + } + + /** + * Accepts an array of values to loop through to build cData from the release info. + * + * @param array $columns The columns in the release we need to insert + * @return string The HTML format cData + */ + protected function buildCdata(array $columns): string + { + $cData = ''; + + foreach ($columns as $info) { + if (! empty($this->release->$info)) { + if ($info === 'mu_releasedate') { + $ucInfo = 'Released'; + $rDate = date('Y-m-d', strtotime($this->release->$info)); + $cData .= "
  • {$ucInfo}: {$rDate}
  • \n"; + } else { + $ucInfo = ucfirst(preg_replace('/^[a-z]{2}_/i', '', $info)); + $cData .= "
  • {$ucInfo}: {$this->release->$info}
  • \n"; + } + } + } + + return $cData; + } } - - /** - * @throws \Exception - * @throws \Throwable - */ - public function tv(Request $request): JsonResponse - { - if ($request->missing('api_token') || $request->isNotFilled('api_token')) { - return response()->json(['error' => 'Missing parameter (api_token)'], 403); - } - $releases = new Releases; - $user = User::query()->where('api_token', $request->input('api_token'))->first(); - if ($user === null) { - return response()->json(['error' => 'Invalid API Token'], 403); - } - $catExclusions = User::getCategoryExclusionForApi($request); - $minSize = $request->has('minsize') && $request->input('minsize') > 0 ? $request->input('minsize') : 0; - $this->api->verifyEmptyParameter($request, 'id'); - $this->api->verifyEmptyParameter($request, 'vid'); - $this->api->verifyEmptyParameter($request, 'tvdbid'); - $this->api->verifyEmptyParameter($request, 'traktid'); - $this->api->verifyEmptyParameter($request, 'rid'); - $this->api->verifyEmptyParameter($request, 'tvmazeid'); - $this->api->verifyEmptyParameter($request, 'imdbid'); - $this->api->verifyEmptyParameter($request, 'tmdbid'); - $this->api->verifyEmptyParameter($request, 'season'); - $this->api->verifyEmptyParameter($request, 'ep'); - $maxAge = $this->api->maxAge($request); - UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri()); - event(new UserAccessedApi($user)); - - $siteIdArr = [ - 'id' => $request->input('vid') ?? null, - 'tvdb' => $request->input('tvdbid') ?? null, - 'trakt' => $request->input('traktid') ?? null, - 'tvrage' => $request->input('rid') ?? null, - 'tvmaze' => $request->input('tvmazeid') ?? null, - 'imdb' => $request->input('imdbid') ?? null, - 'tmdb' => $request->input('tmdbid') ?? null, - ]; - - // Process season only queries or Season and Episode/Airdate queries - - $series = $request->input('season') ?? ''; - $episode = $request->input('ep') ?? ''; - - if (preg_match('#^(19|20)\d{2}$#', $series, $year) && str_contains($episode, '/')) { - $airDate = str_replace('/', '-', $year[0].'-'.$episode); - } - - $relData = $releases->apiTvSearch( - $siteIdArr, - $series, - $episode, - $airDate ?? '', - $this->api->offset($request), - $this->api->limit($request), - $request->input('id') ?? '', - $this->api->categoryID($request), - $maxAge, - $minSize, - $catExclusions - ); - - $time = UserRequest::whereUsersId($user->id)->min('timestamp'); - $apiOldestTime = $time !== null ? Carbon::createFromTimeString($time)->toRfc2822String() : ''; - $grabTime = UserDownload::whereUsersId($user->id)->min('timestamp'); - $oldestGrabTime = $grabTime !== null ? Carbon::createFromTimeString($grabTime)->toRfc2822String() : ''; - - $response = [ - 'Total' => $relData[0]->_totalrows ?? 0, - 'apiCurrent' => UserRequest::getApiRequests($user->id), - 'apiMax' => $user->role->apirequests, - 'grabCurrent' => UserDownload::getDownloadRequests($user->id), - 'grabMax' => $user->role->downloadrequests, - 'apiOldestTime' => $apiOldestTime, - 'grabOldestTime' => $oldestGrabTime, - 'Results' => fractal($relData, new ApiTransformer($user)), - ]; - - return response()->json($response); - } - - public function getNzb(Request $request): \Illuminate\Foundation\Application|JsonResponse|\Illuminate\Routing\Redirector|RedirectResponse|\Illuminate\Contracts\Foundation\Application - { - if ($request->missing('api_token') || $request->isNotFilled('api_token')) { - return response()->json(['error' => 'Missing parameter (api_token)'], 403); - } - $user = User::query()->where('api_token', $request->input('api_token'))->first(); - if ($user === null) { - return response()->json(['error' => 'Invalid API Token'], 403); - } - event(new UserAccessedApi($user)); - UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri()); - $relData = Release::checkGuidForApi($request->input('id')); - if ($relData) { - return redirect('/getnzb?r='.$request->input('api_token').'&id='.$request->input('id').(($request->has('del') && $request->input('del') === '1') ? '&del=1' : '')); - } - - return response()->json(['data' => 'No such item (the guid you provided has no release in our database)'], 404); - } - - public function details(Request $request): JsonResponse - { - if ($request->missing('api_token') || $request->isNotFilled('api_token')) { - return response()->json(['error' => 'Missing parameter (api_token)'], 403); - } - if ($request->missing('id')) { - return response()->json(['error' => 'Missing parameter (guid is required for single release details)'], 400); - } - - UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri()); - $user = User::query()->where('api_token', $request->input('api_token'))->first(); - if ($user === null) { - return response()->json(['error' => 'Invalid API Token'], 403); - } - event(new UserAccessedApi($user)); - $relData = Release::getByGuid($request->input('id')); - - $relData = fractal($relData, new DetailsTransformer($user)); - - return response()->json($relData); - } -} diff --git a/app/Transformers/ApiTransformer.php b/app/Transformers/ApiTransformer.php index dbf1c7f76..ec2022841 100644 --- a/app/Transformers/ApiTransformer.php +++ b/app/Transformers/ApiTransformer.php @@ -1,92 +1,92 @@ user = $user; - } + protected $user; + + /** + * ApiTransformer constructor. + */ + public function __construct($user) + { + $this->user = $user; + } + + public function transform(Release $releases): array + { + if (\in_array($releases->categories_id, Category::MOVIES_GROUP, false)) { + return [ + 'title' => $releases->searchname, + 'details' => url('/').'/details/'.$releases->guid, + 'url' => url('/').'/getnzb?id='.$releases->guid.'.nzb'.'&r='.$this->user->api_token, + 'category' => $releases->categories_id, + 'category_name' => $releases->category_name, + 'added' => Carbon::parse($releases->adddate)->toRssString(), + 'size' => $releases->size, + 'files' => $releases->totalpart, + 'poster' => $releases->fromname, + 'imdbid' => $releases->imdbid !== null && $releases->imdbid !== 0 ? $releases->imdbid : $this->null(), + 'tmdbid' => $releases->tmdbid !== null && $releases->tmdbid !== 0 ? $releases->tmdbid : $this->null(), + 'traktid' => $releases->traktid !== null && $releases->traktid !== 0 ? $releases->traktid : $this->null(), + 'grabs' => $releases->grabs !== 0 ? $releases->grabs : $this->null(), + 'comments' => $releases->comments !== 0 ? $releases->comments : $this->null(), + 'password' => $releases->passwordstatus, + 'usenetdate' => Carbon::parse($releases->postdate)->toRssString(), + 'group' => $releases->group_name, + ]; + } + + if (\in_array($releases->categories_id, Category::TV_GROUP, false)) { + return [ + 'title' => $releases->searchname, + 'details' => url('/').'/details/'.$releases->guid, + 'url' => url('/').'/getnzb?id='.$releases->guid.'.nzb'.'&r='.$this->user->api_token, + 'category' => $releases->categories_id, + 'category_name' => $releases->category_name, + 'added' => Carbon::parse($releases->adddate)->toRssString(), + 'size' => $releases->size, + 'files' => $releases->totalpart, + 'poster' => $releases->fromname, + 'episode_title' => $releases->title ?? $this->null(), + 'season' => $releases->series ?? $this->null(), + 'episode' => $releases->episode ?? $this->null(), + 'tvairdate' => $releases->firstaired ?? $this->null(), + 'tvdbid' => $releases->tvdb !== null && $releases->tvdb !== 0 ? $releases->tvdb : $this->null(), + 'traktid' => $releases->trakt !== null && $releases->trakt !== 0 ? $releases->trakt : $this->null(), + 'tvrageid' => $releases->tvrage !== null && $releases->tvrage !== 0 ? $releases->tvrage : $this->null(), + 'tvmazeid' => $releases->tvmaze !== null && $releases->tvmaze !== 0 ? $releases->tvmaze : $this->null(), + 'imdbid' => $releases->imdb !== null && $releases->imdb !== 0 ? $releases->imdb : $this->null(), + 'tmdbid' => $releases->tmdb !== null && $releases->tmdb !== 0 ? $releases->tmdb : $this->null(), + 'grabs' => $releases->grabs !== 0 ? $releases->grabs : $this->null(), + 'comments' => $releases->comments !== 0 ? $releases->comments : $this->null(), + 'password' => $releases->passwordstatus, + 'usenetdate' => Carbon::parse($releases->postdate)->toRssString(), + 'group' => $releases->group_name, + ]; + } - public function transform(Release $releases): array - { - if (\in_array($releases->categories_id, Category::MOVIES_GROUP, false)) { return [ 'title' => $releases->searchname, 'details' => url('/').'/details/'.$releases->guid, 'url' => url('/').'/getnzb?id='.$releases->guid.'.nzb'.'&r='.$this->user->api_token, 'category' => $releases->categories_id, - 'category_name' => $releases->category->parent->title.' > '.$releases->category->title, + 'category_name' => $releases->category_name, 'added' => Carbon::parse($releases->adddate)->toRssString(), 'size' => $releases->size, 'files' => $releases->totalpart, 'poster' => $releases->fromname, - 'imdbid' => $releases->imdbid !== null && $releases->imdbid !== 0 ? $releases->imdbid : $this->null(), - 'tmdbid' => $releases->tmdbid !== null && $releases->tmdbid !== 0 ? $releases->tmdbid : $this->null(), - 'traktid' => $releases->traktid !== null && $releases->traktid !== 0 ? $releases->traktid : $this->null(), 'grabs' => $releases->grabs !== 0 ? $releases->grabs : $this->null(), 'comments' => $releases->comments !== 0 ? $releases->comments : $this->null(), 'password' => $releases->passwordstatus, 'usenetdate' => Carbon::parse($releases->postdate)->toRssString(), - 'group' => $releases->group->name, + 'group' => $releases->group_name, ]; } - - if (\in_array($releases->categories_id, Category::TV_GROUP, false)) { - return [ - 'title' => $releases->searchname, - 'details' => url('/').'/details/'.$releases->guid, - 'url' => url('/').'/getnzb?id='.$releases->guid.'.nzb'.'&r='.$this->user->api_token, - 'category' => $releases->categories_id, - 'category_name' => $releases->category->parent->title.' > '.$releases->category->title, - 'added' => Carbon::parse($releases->adddate)->toRssString(), - 'size' => $releases->size, - 'files' => $releases->totalpart, - 'poster' => $releases->fromname, - 'episode_title' => $releases->episode->title ?? $this->null(), - 'season' => $releases->episode->series ?? $this->null(), - 'episode' => $releases->episode->episode ?? $this->null(), - 'tvairdate' => $releases->episode->firstaired ?? $this->null(), - 'tvdbid' => $releases->video->tvdb !== null && $releases->video->tvdb !== 0 ? $releases->video->tvdb : $this->null(), - 'traktid' => $releases->video->trakt !== null && $releases->video->trakt !== 0 ? $releases->video->trakt : $this->null(), - 'tvrageid' => $releases->video->tvrage !== null && $releases->video->tvrage !== 0 ? $releases->video->tvrage : $this->null(), - 'tvmazeid' => $releases->video->tvmaze !== null && $releases->video->tvmaze !== 0 ? $releases->video->tvmaze : $this->null(), - 'imdbid' => $releases->video->imdb !== null && $releases->video->imdb !== 0 ? $releases->video->imdb : $this->null(), - 'tmdbid' => $releases->video->tmdb !== null && $releases->video->tmdb !== 0 ? $releases->video->tmdb : $this->null(), - 'grabs' => $releases->grabs !== 0 ? $releases->grabs : $this->null(), - 'comments' => $releases->comments !== 0 ? $releases->comments : $this->null(), - 'password' => $releases->passwordstatus, - 'usenetdate' => Carbon::parse($releases->postdate)->toRssString(), - 'group' => $releases->group->name, - ]; - } - - return [ - 'title' => $releases->searchname, - 'details' => url('/').'/details/'.$releases->guid, - 'url' => url('/').'/getnzb?id='.$releases->guid.'.nzb'.'&r='.$this->user->api_token, - 'category' => $releases->categories_id, - 'category_name' => $releases->category->parent->title.' > '.$releases->category->title, - 'added' => Carbon::parse($releases->adddate)->toRssString(), - 'size' => $releases->size, - 'files' => $releases->totalpart, - 'poster' => $releases->fromname, - 'grabs' => $releases->grabs !== 0 ? $releases->grabs : $this->null(), - 'comments' => $releases->comments !== 0 ? $releases->comments : $this->null(), - 'password' => $releases->passwordstatus, - 'usenetdate' => Carbon::parse($releases->postdate)->toRssString(), - 'group' => $releases->group->name, - ]; } -} diff --git a/resources/views/themes/Gentele/browse.tpl b/resources/views/themes/Gentele/browse.tpl index e43ea19dd..3ace2d8d2 100755 --- a/resources/views/themes/Gentele/browse.tpl +++ b/resources/views/themes/Gentele/browse.tpl @@ -1,239 +1,239 @@
    -
    {$site->adbrowse} {if count($results) > 0} - {{Form::open(['id' => 'nzb_multi_operations_form', 'method' => 'get'])}} -
    -
    -
    -
    -
    -
    -
    - {if isset($shows)} -

    - Series List | - Manage - My Shows | - Rss - Feed -

    - {/if} -
    - {if isset($covgroup) && $covgroup != ''}View: - Covers - - | - List -
    - {/if} - With Selected: -
    - - - {if isset($isadmin)} - - - {/if} -
    -
    -
    -
    - {$results->onEachSide(5)->links()} + {{Form::open(['id' => 'nzb_multi_operations_form', 'method' => 'get'])}} +
    +
    +
    +
    +
    +
    +
    + {if isset($shows)} +

    + Series List | + Manage + My Shows | + Rss + Feed +

    + {/if} +
    + {if isset($covgroup) && $covgroup != ''}View: + Covers + + | + List +
    + {/if} + With Selected: +
    + + + {if isset($isadmin)} + + + {/if} +
    -
    -
    -
    - - - - - - - - - - - - - - - {foreach $resultsadd as $result} - - - + + + + + + + + {/foreach} + +
    Name
    - - -
    Category
    - - -
    Posted
    - - -
    Size
    - - -
    Files
    - - -
    Downloads
    - - -
    Action
    - guid}")}}" - class="title">{$result->searchname|escape:"htmlall"|replace:".":" "}{if (count($result->failed)) > 0 } - {/if} -
    - {$result->grabs} + +
    + {$results->onEachSide(5)->links()} +
    + +
    +
    + + + + + + + + + + + + + + + {foreach $resultsadd as $result} + + + - - - - - - - - {/foreach} - -
    Name
    + + +
    Category
    + + +
    Posted
    + + +
    Size
    + + +
    Files
    + + +
    Downloads
    + + +
    Action
    + guid}")}}" + class="title">{$result->searchname|escape:"htmlall"|replace:".":" "}{if !empty($result->failed)} + {/if} +
    + {$result->grabs} Grab{if $result->grabs != 1}s{/if} - {if $result->nfoid > 0}guid}")}}" - class="modal_nfo badge bg-info" rel="nfo">NFO - {/if} - {if $result->jpgstatus == 1 && $userdata->can('preview') == true}guid}_thumb.jpg")}}" - name="name{$result->guid}" - data-fancybox - class="badge bg-info" - rel="preview">Sample{/if} - {if $result->haspreview == 1 && $userdata->can('preview') == true}guid}_thumb.jpg")}}" - name="name{$result->guid}" - data-fancybox - class="badge bg-info" - rel="preview">Preview{/if} - {if $result->videos_id > 0}videos_id}")}}" - class="badge bg-info" rel="series">View TV - {/if} - {if !empty($result->firstaired)} - Aired {if $result->firstaired|strtotime > $smarty.now}in future{else}{$result->firstaired|daysago}{/if}{/if} - {if $result->anidbid > 0}anidbid}")}}">View + {if $result->nfoid > 0}guid}")}}" + class="modal_nfo badge bg-info" rel="nfo">NFO + {/if} + {if $result->jpgstatus == 1 && $userdata->can('preview') == true}guid}_thumb.jpg")}}" + name="name{$result->guid}" + data-fancybox + class="badge bg-info" + rel="preview">Sample{/if} + {if $result->haspreview == 1 && $userdata->can('preview') == true}guid}_thumb.jpg")}}" + name="name{$result->guid}" + data-fancybox + class="badge bg-info" + rel="preview">Preview{/if} + {if $result->videos_id > 0}videos_id}")}}" + class="badge bg-info" rel="series">View TV + {/if} + {if !empty($result->firstaired)} + Aired {if $result->firstaired|strtotime > $smarty.now}in future{else}{$result->firstaired|daysago}{/if}{/if} + {if $result->anidbid > 0}anidbid}")}}">View Anime{/if} - {if count($result->failed) > 0} - - {$result->grabs} Grab{if $result->grabs != 1}s{/if} / - - {$result->failed} Failed Download{if $result->failed != 1}s{/if} - {/if} - {$result->group_name} - {$result->fromname} - {if $lastvisit|strtotime<$result->adddate|strtotime} - New - {/if} -
    {$result->category->parent->title} > {$result->category->title} - {$result->postdate|timeago}{$result->size|filesize} - {$result->totalpart} - - {if $result->rarinnerfilecount > 0} -
    - -
    - {/if} -
    {$result->grabs} - guid}")}}" - class="icon_nzb text-muted"> - guid}/#comments")}}"> - -
    -
    -
    -
    -
    -
    - {if isset($covgroup) && $covgroup != ''}View: - Covers - | - List -
    - {/if} - With Selected: -
    - - - {if isset($isadmin)} - - - {/if} -
    -
    -
    -
    - {$results->onEachSide(5)->links()} -
    -
    - - - - - - {{Form::close()}} + {if !empty($result->failed)} + + {$result->grabs} Grab{if $result->grabs != 1}s{/if} / + + {$result->failed} Failed Download{if $result->failed != 1}s{/if} + {/if} + {$result->group_name} + {$result->fromname} + {if $lastvisit|strtotime<$result->adddate|strtotime} + New + {/if} +
    {$result->category_name} + {$result->postdate|timeago}{$result->size|filesize} + {$result->totalpart} + + {if $result->rarinnerfilecount > 0} +
    + +
    + {/if} +
    {$result->grabs} + guid}")}}" + class="icon_nzb text-muted"> + guid}/#comments")}}"> + +
    +
    +
    +
    +
    +
    + {if isset($covgroup) && $covgroup != ''}View: + Covers + | + List +
    + {/if} + With Selected: +
    + + + {if isset($isadmin)} + + + {/if} +
    +
    +
    +
    + {$results->onEachSide(5)->links()} +
    +
    +
    +
    +
    +
    +
    + {{Form::close()}} {else} No releases indexed yet! {/if} diff --git a/resources/views/themes/Gentele/search.tpl b/resources/views/themes/Gentele/search.tpl index b236226e3..ab8e7c0e6 100755 --- a/resources/views/themes/Gentele/search.tpl +++ b/resources/views/themes/Gentele/search.tpl @@ -1,406 +1,406 @@
    - {if {$site->adbrowse} != ''} - {$site->adbrowse} - {/if} -
    -

    {$site->title} > Search

    - -
    -
    - + -
    -
    -
    -
    -
    - {{Form::open(['url' => 'search', 'method' => 'get'])}} -
    -
    - -
    -
    - - - {{Form::submit('Search', ['class' => 'btn btn-success', 'id' => 'search_search_button'])}} -
    -
    - {{Form::close()}} -
    - {{Form::open(['url' => 'search', 'method' => 'get'])}} -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    {html_options class="searchadvbtns" id="searchadvgroups" name="searchadvgroups" options=$grouplist selected=$selectedgroup}
    {html_options class="searchadvbtns" id="searchadvcat" name="searchadvcat" options=$catlist selected=$selectedcat}
    - {html_options id="searchadvsizefrom" name="searchadvsizefrom" options=$sizelist selected=$selectedsizefrom} - {html_options id="searchadvsizeto" name="searchadvsizeto" options=$sizelist selected=$selectedsizeto} -
    -
    - {{Form::submit('Search', ['class' => 'btn btn-success', 'id' => 'search_adv_button'])}} -
    -
    -
    -
    - {{Form::close()}} - {if $results|@count == 0 && ($search || $subject|| $searchadvr|| $searchadvsubject || $selectedgroup || $selectedsizefrom || $searchadvdaysold) != ""} -
    -
    - Your search did not match any releases. -

    - Suggestions: -

    -
      -
      -
    • Make sure all words are spelled correctly.
    • -
      -
      -
    • Try different keywords.
    • -
      -
      -
    • Try more general keywords.
    • -
      -
      -
    • Try fewer keywords.
    • -
      -
    -
    -
    - {elseif ($search || $subject || $searchadvr || $searchadvsubject || $selectedgroup || $selectedsizefrom || $searchadvdaysold) == ""} - {else} -
    - {{Form::open(['id' => 'nzb_multi_operations_form','style' => 'padding-top:10px;', 'method' => 'get', 'url' => 'search'])}} -
    -
    - {if isset($shows)} -

    - Series List | - Manage My Shows | - Rss - Feed -

    - {/if} -
    - {if isset($section) && $section != ''}View: - Covers - | - List -
    - {/if} - With Selected: -
    - - - {if isset($isadmin)} - - - {/if} -
    -
    -
    - {if count($results) > 0} -
    - {$results->onEachSide(5)->links()} -
    - {/if} -
    -
    - - - - - - - - - - - - - - - {foreach $results as $result} - adddate|strtotime} new{/if}" - id="guid{$result->guid}"> - - - - - - - - - - {/foreach} - -
    Name
    - - -
    Category
    - - -
    Posted
    - - -
    Size
    - - -
    Files
    - - -
    Downloads
    - - -
    Action
    - - - -
    -
    - {release_flag($result->searchname, browse)} - {if $result->passwordstatus == 1} - RAR/ZIP is Passworded. - {/if} - {if $result->videostatus > 0} - guid}")}}" - title="This release has a video preview." - rel="preview" - > - - {/if} - {if $result->nfoid > 0} - guid}")}}" - title="View Nfo" - class="modal_nfo badge bg-info" rel="nfo">Nfo - {/if} - {if $result->imdbid > 0} - Cover - {/if} - {if $result->haspreview == 1 && $userdata->can('preview') == true} - guid}_thumb.jpg")}}" - name="name{$result->guid}" - data-fancybox - title="Screenshot of {$result->searchname|escape:"htmlall"}" - class="badge bg-info" rel="preview">Preview{/if} - {if $result->jpgstatus == 1 && $userdata->can('preview') == true} - guid}_thumb.jpg")}}" - name="name{$result->guid}" - data-fancybox - title="Sample of {$result->searchname|escape:"htmlall"}" - class="badge bg-info" rel="preview">Sample{/if} - {if $result->musicinfo_id > 0} - Cover - {/if} - {if $result->consoleinfo_id > 0} - Cover - {/if} - {if $result->videos_id > 0} - videos_id}")}}" - title="View all episodes">View - Series - {/if} - {if $result->anidbid > 0} - anidbid}")}}" - title="View all episodes">View - Anime - {/if} - {if isset($result->firstaired) && $result->firstaired != ''} - Aired {if $result->firstaired|strtotime > $smarty.now}in future{else}{$result->firstaired|daysago}{/if} - {/if} - {if $result->group_name != ""} - group_name|escape:"htmlall"}")}}" - title="Browse {$result->group_name}">{$result->group_name|escape:"htmlall"|replace:"alt.binaries.":"a.b."} - {/if} - {if count($result->failed) > 0} - - {$result->grabs} Grab{if $result->grabs != 1}s{/if} / - - {$result->failed} Failed Download{if $result->failed != 1}s{/if} - - {/if} -
    -
    -
    - category->parent->title}/{$result->category->title}")}}"> {$result->category->parent->title} > {$result->category->title} - - {$result->postdate|timeago} - - {$result->size|filesize} - {if $result->completion > 0} -
    - {if $result->completion < 100} - {$result->completion}% - {else} - {$result->completion}% - {/if} - {/if} -
    - guid}")}}">{$result->totalpart} - {if $result->rarinnerfilecount > 0} -
    - {$result->guid} -
    - {/if} -
    - guid}/#comments")}}">{$result->comments} - cmt{if $result->comments != 1}s{/if} -
    {$result->grabs} grab{if $result->grabs != 1}s{/if} -
    - guid}")}}" - class="icon_nzb text-muted"> - guid}/#comments")}}"> - -
    -
    -
    -
    -
    - With Selected: -
    - - - {if isset($isadmin)} - - - {/if} -
    -
    -
    - {if count($results) > 0} -
    - {$results->onEachSide(5)->links()} -
    - {/if} -
    -


    - {{Form::close()}} -
    - {/if} -
    + +
    +
    +
    +
    +
    +
    +
    + {{Form::open(['url' => 'search', 'method' => 'get'])}} +
    +
    + +
    +
    + + + {{Form::submit('Search', ['class' => 'btn btn-success', 'id' => 'search_search_button'])}} +
    +
    + {{Form::close()}} +
    + {{Form::open(['url' => 'search', 'method' => 'get'])}} +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    {html_options class="searchadvbtns" id="searchadvgroups" name="searchadvgroups" options=$grouplist selected=$selectedgroup}
    {html_options class="searchadvbtns" id="searchadvcat" name="searchadvcat" options=$catlist selected=$selectedcat}
    + {html_options id="searchadvsizefrom" name="searchadvsizefrom" options=$sizelist selected=$selectedsizefrom} + {html_options id="searchadvsizeto" name="searchadvsizeto" options=$sizelist selected=$selectedsizeto} +
    +
    + {{Form::submit('Search', ['class' => 'btn btn-success', 'id' => 'search_adv_button'])}} +
    +
    +
    +
    + {{Form::close()}} + {if $results|@count == 0 && ($search || $subject|| $searchadvr|| $searchadvsubject || $selectedgroup || $selectedsizefrom || $searchadvdaysold) != ""} +
    +
    + Your search did not match any releases. +

    + Suggestions: +

    +
      +
      +
    • Make sure all words are spelled correctly.
    • +
      +
      +
    • Try different keywords.
    • +
      +
      +
    • Try more general keywords.
    • +
      +
      +
    • Try fewer keywords.
    • +
      +
    +
    +
    + {elseif ($search || $subject || $searchadvr || $searchadvsubject || $selectedgroup || $selectedsizefrom || $searchadvdaysold) == ""} + {else} +
    + {{Form::open(['id' => 'nzb_multi_operations_form','style' => 'padding-top:10px;', 'method' => 'get', 'url' => 'search'])}} +
    +
    + {if isset($shows)} +

    + Series List | + Manage My Shows | + Rss + Feed +

    + {/if} +
    + {if isset($section) && $section != ''}View: + Covers + | + List +
    + {/if} + With Selected: +
    + + + {if isset($isadmin)} + + + {/if} +
    +
    +
    + {if count($results) > 0} +
    + {$results->onEachSide(5)->links()} +
    + {/if} +
    +
    + + + + + + + + + + + + + + + {foreach $results as $result} + adddate|strtotime} new{/if}" + id="guid{$result->guid}"> + + + + + + + + + + {/foreach} + +
    Name
    + + +
    Category
    + + +
    Posted
    + + +
    Size
    + + +
    Files
    + + +
    Downloads
    + + +
    Action
    + + + +
    +
    + {release_flag($result->searchname, browse)} + {if $result->passwordstatus == 1} + RAR/ZIP is Passworded. + {/if} + {if $result->videostatus > 0} + guid}")}}" + title="This release has a video preview." + rel="preview" + > + + {/if} + {if $result->nfoid > 0} + guid}")}}" + title="View Nfo" + class="modal_nfo badge bg-info" rel="nfo">Nfo + {/if} + {if $result->imdbid > 0} + Cover + {/if} + {if $result->haspreview == 1 && $userdata->can('preview') == true} + guid}_thumb.jpg")}}" + name="name{$result->guid}" + data-fancybox + title="Screenshot of {$result->searchname|escape:"htmlall"}" + class="badge bg-info" rel="preview">Preview{/if} + {if $result->jpgstatus == 1 && $userdata->can('preview') == true} + guid}_thumb.jpg")}}" + name="name{$result->guid}" + data-fancybox + title="Sample of {$result->searchname|escape:"htmlall"}" + class="badge bg-info" rel="preview">Sample{/if} + {if $result->musicinfo_id > 0} + Cover + {/if} + {if $result->consoleinfo_id > 0} + Cover + {/if} + {if $result->videos_id > 0} + videos_id}")}}" + title="View all episodes">View + Series + {/if} + {if $result->anidbid > 0} + anidbid}")}}" + title="View all episodes">View + Anime + {/if} + {if isset($result->firstaired) && $result->firstaired != ''} + Aired {if $result->firstaired|strtotime > $smarty.now}in future{else}{$result->firstaired|daysago}{/if} + {/if} + {if $result->group_name != ""} + group_name|escape:"htmlall"}")}}" + title="Browse {$result->group_name}">{$result->group_name|escape:"htmlall"|replace:"alt.binaries.":"a.b."} + {/if} + {if !empty($result->failed)} + + {$result->grabs} Grab{if $result->grabs != 1}s{/if} / + + {$result->failed} Failed Download{if $result->failed != 1}s{/if} + + {/if} +
    +
    +
    + parent_category}/{$result->sub_category}")}}"> {$result->category_name} + + {$result->postdate|timeago} + + {$result->size|filesize} + {if $result->completion > 0} +
    + {if $result->completion < 100} + {$result->completion}% + {else} + {$result->completion}% + {/if} + {/if} +
    + guid}")}}">{$result->totalpart} + {if $result->rarinnerfilecount > 0} +
    + {$result->guid} +
    + {/if} +
    + guid}/#comments")}}">{$result->comments} + cmt{if $result->comments != 1}s{/if} +
    {$result->grabs} grab{if $result->grabs != 1}s{/if} +
    + guid}")}}" + class="icon_nzb text-muted"> + guid}/#comments")}}"> + +
    +
    +
    +
    +
    + With Selected: +
    + + + {if isset($isadmin)} + + + {/if} +
    +
    +
    + {if count($results) > 0} +
    + {$results->onEachSide(5)->links()} +
    + {/if} +
    +


    + {{Form::close()}} +
    + {/if} +