Improvew API response

This commit is contained in:
DariusIII
2026-02-12 12:48:09 +01:00
parent e6028dbcbe
commit 935bae2a45
11 changed files with 638 additions and 261 deletions
+14 -2
View File
@@ -786,10 +786,15 @@ if (! function_exists('streamSslContextOptions')) {
if (! function_exists('getCoverURL')) {
/**
* Get cover URL for a release. Uses a short-lived in-memory cache to avoid
* repeated filesystem file_exists() calls for the same cover during a single request.
*
* @param array<string, mixed> $options
*/
function getCoverURL(array $options = []): string
{
static $coverCache = [];
$defaults = [
'id' => null,
'suffix' => '-cover.jpg',
@@ -806,8 +811,15 @@ if (! function_exists('getCoverURL')) {
)
) {
$fileSpec = sprintf($fileSpecTemplate, $options['type'], $options['id'], $options['suffix']);
$fileSpec = file_exists(storage_path('covers/').$fileSpec) ? $fileSpec :
sprintf($fileSpecTemplate, $options['type'], 'no', $options['suffix']);
$cacheKey = $options['type'].':'.$options['id'];
if (! isset($coverCache[$cacheKey])) {
$coverCache[$cacheKey] = file_exists(storage_path('covers/').$fileSpec);
}
if (! $coverCache[$cacheKey]) {
$fileSpec = sprintf($fileSpecTemplate, $options['type'], 'no', $options['suffix']);
}
}
return $fileSpec;
+95 -50
View File
@@ -10,7 +10,6 @@ use App\Models\ReleaseNfo;
use App\Models\Settings;
use App\Models\UsenetGroup;
use App\Models\User;
use App\Models\UserDownload;
use App\Models\UserRequest;
use App\Services\Releases\ReleaseBrowseService;
use App\Services\Releases\ReleaseSearchService;
@@ -20,6 +19,8 @@ use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
@@ -104,7 +105,16 @@ class ApiController extends BasePageController
}
$apiKey = $request->input('apikey');
$res = User::getByRssToken($apiKey);
// Cache user lookup for 5 minutes to avoid repeated DB hits (same pattern as API v2)
$userCacheKey = 'api_user:'.md5($apiKey);
$res = Cache::remember($userCacheKey, 300, function () use ($apiKey) {
return User::query()
->where('api_token', $apiKey)
->with('role')
->first();
});
if ($res === null) {
return showApiError(100, 'Incorrect user credentials (wrong API key)');
}
@@ -114,20 +124,22 @@ class ApiController extends BasePageController
}
$uid = $res->id;
$catExclusions = User::getCategoryExclusionForApi($request);
// Use user ID directly instead of re-looking up by token
$catExclusions = User::getCategoryExclusionById($uid);
$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() : '';
// Consolidated user stats: single query with 60s cache instead of 4 separate queries
$userStats = $this->getCachedUserStats($uid);
$thisOldestTime = $userStats->api_time ? Carbon::parse($userStats->api_time)->toRfc2822String() : '';
$oldestGrabTime = $userStats->grab_time ? Carbon::parse($userStats->grab_time)->toRfc2822String() : '';
$thisRequests = (int) ($userStats->api_count ?? 0);
$grabs = (int) ($userStats->grab_count ?? 0);
}
// 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, $request->ip()));
$thisRequests = UserRequest::getApiRequests($uid);
$grabs = UserDownload::getDownloadRequests($uid);
if ($thisRequests > $maxRequests) {
return showApiError(500, 'Request limit reached ('.$thisRequests.'/'.$maxRequests.')');
}
@@ -156,7 +168,7 @@ class ApiController extends BasePageController
$this->verifyEmptyParameter($request, 'q');
$maxAge = $this->maxAge($request);
$groupName = $this->group($request);
UserRequest::addApiRequest($apiKey, $request->getRequestUri());
UserRequest::addApiRequest($uid, $request->getRequestUri());
$categoryID = $this->categoryID($request);
$limit = $this->limit($request);
@@ -199,7 +211,7 @@ class ApiController extends BasePageController
$this->verifyEmptyParameter($request, 'season');
$this->verifyEmptyParameter($request, 'ep');
$maxAge = $this->maxAge($request);
UserRequest::addApiRequest($apiKey, $request->getRequestUri());
UserRequest::addApiRequest($uid, $request->getRequestUri());
$siteIdArr = [
'id' => $request->input('vid') ?? '0',
@@ -243,7 +255,7 @@ class ApiController extends BasePageController
$this->verifyEmptyParameter($request, 'q');
$this->verifyEmptyParameter($request, 'imdbid');
$maxAge = $this->maxAge($request);
UserRequest::addApiRequest($apiKey, $request->getRequestUri());
UserRequest::addApiRequest($uid, $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;
@@ -275,7 +287,7 @@ class ApiController extends BasePageController
// Get NZB.
case 'g':
$this->verifyEmptyParameter($request, 'g');
UserRequest::addApiRequest($apiKey, $request->getRequestUri());
UserRequest::addApiRequest($uid, $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' : '')));
@@ -289,8 +301,8 @@ class ApiController extends BasePageController
return showApiError(200, 'Missing parameter (guid is required for single release details)');
}
UserRequest::addApiRequest($apiKey, $request->getRequestUri());
$data = Release::getByGuid($request->input('id'));
UserRequest::addApiRequest($uid, $request->getRequestUri());
$data = Release::getByGuidForApi($request->input('id'));
$this->output($data, $params, $outputXML, $offset, 'api');
break;
@@ -301,7 +313,7 @@ class ApiController extends BasePageController
return showApiError(200, 'Missing parameter (id is required for retrieving an NFO)');
}
UserRequest::addApiRequest($apiKey, $request->getRequestUri());
UserRequest::addApiRequest($uid, $request->getRequestUri());
$rel = Release::query()->where('guid', $request->input('id'))->first(['id', 'searchname']);
if ($rel) {
@@ -341,7 +353,7 @@ class ApiController extends BasePageController
return response('Missing parameter (file is required for adding an NZB)', 400);
}
UserRequest::addApiRequest($apiKey, $request->getRequestUri());
UserRequest::addApiRequest($uid, $request->getRequestUri());
$nzbFile = $request->file('file');
@@ -398,14 +410,19 @@ class ApiController extends BasePageController
'Type' => $type,
];
// Generate the XML Response
$response = (new XML_Response($options))->returnXML();
$xmlResponse = new XML_Response($options);
if ($xml) {
// Generate XML response
$response = $xmlResponse->returnXML();
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);
// Build JSON directly from array (avoids expensive XML->xml_to_array->json_encode path)
$arrayData = $xmlResponse->returnArray();
if ($arrayData === false) {
return showApiError(201);
}
$response = json_encode($arrayData, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
header('Content-type: application/json');
}
if ($response === false) {
@@ -419,7 +436,7 @@ class ApiController extends BasePageController
/**
* Collect and return various capability information for usage in API.
*
* Cached for 10 minutes to avoid repeated Settings DB lookups on every API response.
*
* @return array<string, mixed>
*
@@ -427,35 +444,42 @@ class ApiController extends BasePageController
*/
public function getForMenu(): array
{
$serverroot = url('/');
$includeCats = $this->type === 'caps';
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,
];
// Cache the server info blob (without categories) for 10 minutes
$serverInfo = Cache::remember('api_v1_server_menu', 600, function () {
$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' => ''],
],
];
});
// Only load categories for caps requests (also cached via Category::getForMenu)
$serverInfo['categories'] = $includeCats ? Category::getForMenu() : null;
return $serverInfo;
}
/**
@@ -556,6 +580,27 @@ class ApiController extends BasePageController
}
}
/**
* Get cached user stats (API requests + download counts/timestamps) in a single query.
* Cached for 60 seconds to reduce DB hits across rapid API calls.
*/
public function getCachedUserStats(int $userId): object
{
$cacheKey = 'api_user_stats:'.$userId;
return Cache::remember($cacheKey, 60, function () use ($userId) {
$oneDayAgo = now()->subDay()->toDateTimeString();
return DB::selectOne('
SELECT
(SELECT COUNT(*) FROM user_requests WHERE users_id = ? AND timestamp > ?) as api_count,
(SELECT COUNT(*) FROM user_downloads WHERE users_id = ? AND timestamp > ?) as grab_count,
(SELECT MIN(timestamp) FROM user_requests WHERE users_id = ? AND timestamp > ?) as api_time,
(SELECT MIN(timestamp) FROM user_downloads WHERE users_id = ? AND timestamp > ?) as grab_time
', [$userId, $oneDayAgo, $userId, $oneDayAgo, $userId, $oneDayAgo, $userId, $oneDayAgo]);
});
}
public function addCoverURL(mixed &$releases, callable $getCoverURL): void
{
if ($releases && \count($releases)) {
+111 -130
View File
@@ -8,7 +8,6 @@ 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\Services\Releases\ReleaseBrowseService;
use App\Services\Releases\ReleaseSearchService;
@@ -20,7 +19,6 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ApiV2Controller extends BasePageController
{
@@ -40,33 +38,77 @@ class ApiV2Controller extends BasePageController
$this->releaseBrowseService = $releaseBrowseService;
}
/**
* Validate API token and return cached user, or null on failure.
* Caches user lookup for 5 minutes to reduce DB hits.
*/
private function resolveUser(Request $request): ?User
{
if ($request->missing('api_token') || $request->isNotFilled('api_token')) {
return null;
}
$apiToken = $request->input('api_token');
$userCacheKey = 'api_user:'.md5($apiToken);
return Cache::remember($userCacheKey, 300, function () use ($apiToken) {
return User::query()
->where('api_token', $apiToken)
->with('role')
->first();
});
}
/**
* Build the standard user stats portion of an API response.
* Uses the consolidated single-query + 60s cache from ApiController.
*
* @return array<string, mixed>
*/
private function buildUserStatsResponse(User $user): array
{
$userStats = $this->api->getCachedUserStats($user->id);
return [
'apiCurrent' => (int) ($userStats->api_count ?? 0),
'apiMax' => $user->role->apirequests,
'grabCurrent' => (int) ($userStats->grab_count ?? 0),
'grabMax' => $user->role->downloadrequests,
'apiOldestTime' => $userStats->api_time ? Carbon::parse($userStats->api_time)->toRfc2822String() : '',
'grabOldestTime' => $userStats->grab_time ? Carbon::parse($userStats->grab_time)->toRfc2822String() : '',
];
}
public function capabilities(): JsonResponse
{
$category = Category::getForApi();
// Cache the full capabilities response for 10 minutes
$capabilities = Cache::remember('api_v2_capabilities', 600, function () {
$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 [
'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);
}
@@ -76,28 +118,12 @@ class ApiV2Controller extends BasePageController
*/
public function movie(Request $request): JsonResponse
{
// Validate API token and get user in one query
if ($request->missing('api_token') || $request->isNotFilled('api_token')) {
return response()->json(['error' => 'Missing parameter (apikey)'], 403);
}
$apiToken = $request->input('api_token');
// Cache user lookup for 5 minutes to reduce DB hits
$userCacheKey = 'api_user:'.md5($apiToken);
$user = Cache::remember($userCacheKey, 300, function () use ($apiToken) {
return User::query()
->where('api_token', $apiToken)
->with('role')
->first();
});
$user = $this->resolveUser($request);
if (! $user) {
return response()->json(['error' => 'Invalid API key'], 403);
return response()->json(['error' => 'Missing or invalid API key'], 403);
}
// Queue API request logging asynchronously (non-blocking)
UserRequest::addApiRequest($apiToken, $request->getRequestUri());
UserRequest::addApiRequest($user->id, $request->getRequestUri());
event(new UserAccessedApi($user, $request->ip()));
// Get request parameters efficiently
@@ -110,7 +136,7 @@ class ApiV2Controller extends BasePageController
$limit = $this->api->limit($request);
$categoryID = $this->api->categoryID($request);
$maxAge = $this->api->maxAge($request);
$catExclusions = User::getCategoryExclusionForApi($request);
$catExclusions = User::getCategoryExclusionById($user->id);
// Create cache key for movie search results
$searchCacheKey = 'api_movie_search:'.md5(serialize([
@@ -137,31 +163,11 @@ class ApiV2Controller extends BasePageController
);
});
// Get user stats with a single optimized raw SQL query
$userStatsCacheKey = 'api_user_stats:'.$user->id;
$userStats = Cache::remember($userStatsCacheKey, 60, function () use ($user) {
$oneDayAgo = now()->subDay()->toDateTimeString();
return DB::selectOne('
SELECT
(SELECT COUNT(*) FROM user_requests WHERE users_id = ? AND timestamp > ?) as api_count,
(SELECT COUNT(*) FROM user_downloads WHERE users_id = ? AND timestamp > ?) as grab_count,
(SELECT MIN(timestamp) FROM user_requests WHERE users_id = ? AND timestamp > ?) as api_time,
(SELECT MIN(timestamp) FROM user_downloads WHERE users_id = ? AND timestamp > ?) as grab_time
', [$user->id, $oneDayAgo, $user->id, $oneDayAgo, $user->id, $oneDayAgo, $user->id, $oneDayAgo]);
});
// Build response
$response = [
'Total' => $relData[0]->_totalrows ?? 0,
'apiCurrent' => (int) ($userStats->api_count ?? 0),
'apiMax' => $user->role->apirequests,
'grabCurrent' => (int) ($userStats->grab_count ?? 0),
'grabMax' => $user->role->downloadrequests,
'apiOldestTime' => $userStats->api_time ? Carbon::parse($userStats->api_time)->toRfc2822String() : '',
'grabOldestTime' => $userStats->grab_time ? Carbon::parse($userStats->grab_time)->toRfc2822String() : '',
'Results' => fractal($relData, new ApiTransformer($user)),
];
$response = array_merge(
['Total' => $relData[0]->_totalrows ?? 0],
$this->buildUserStatsResponse($user),
['Results' => fractal($relData, new ApiTransformer($user))]
);
return response()->json($response);
}
@@ -172,17 +178,19 @@ class ApiV2Controller extends BasePageController
*/
public function apiSearch(Request $request): JsonResponse
{
if ($request->missing('api_token') || $request->isNotFilled('api_token')) {
return response()->json(['error' => 'Missing parameter (api_token)'], 403);
$user = $this->resolveUser($request);
if (! $user) {
return response()->json(['error' => 'Missing or invalid API key'], 403);
}
$user = User::query()->where('api_token', $request->input('api_token'))->first();
UserRequest::addApiRequest($user->id, $request->getRequestUri());
event(new UserAccessedApi($user, $request->ip()));
$offset = $this->api->offset($request);
$catExclusions = User::getCategoryExclusionForApi($request);
$catExclusions = User::getCategoryExclusionById($user->id);
$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, $request->ip()));
$categoryID = $this->api->categoryID($request);
$limit = $this->api->limit($request);
@@ -211,21 +219,11 @@ class ApiV2Controller extends BasePageController
);
}
$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)),
];
$response = array_merge(
['Total' => $relData[0]->_totalrows ?? 0],
$this->buildUserStatsResponse($user),
['Results' => fractal($relData, new ApiTransformer($user))]
);
return response()->json($response);
}
@@ -236,14 +234,12 @@ class ApiV2Controller extends BasePageController
*/
public function tv(Request $request): JsonResponse
{
if ($request->missing('api_token') || $request->isNotFilled('api_token')) {
return response()->json(['error' => 'Missing parameter (api_token)'], 403);
$user = $this->resolveUser($request);
if (! $user) {
return response()->json(['error' => 'Missing or invalid API key'], 403);
}
$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);
$catExclusions = User::getCategoryExclusionById($user->id);
$minSize = $request->has('minsize') && $request->input('minsize') > 0 ? $request->input('minsize') : 0;
$this->api->verifyEmptyParameter($request, 'id');
$this->api->verifyEmptyParameter($request, 'vid');
@@ -256,7 +252,7 @@ class ApiV2Controller extends BasePageController
$this->api->verifyEmptyParameter($request, 'season');
$this->api->verifyEmptyParameter($request, 'ep');
$maxAge = $this->api->maxAge($request);
UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri());
UserRequest::addApiRequest($user->id, $request->getRequestUri());
event(new UserAccessedApi($user, $request->ip()));
$siteIdArr = [
@@ -292,36 +288,24 @@ class ApiV2Controller extends BasePageController
$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)),
];
$response = array_merge(
['Total' => $relData[0]->_totalrows ?? 0],
$this->buildUserStatsResponse($user),
['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);
$user = $this->resolveUser($request);
if (! $user) {
return response()->json(['error' => 'Missing or invalid API key'], 403);
}
event(new UserAccessedApi($user, $request->ip()));
UserRequest::addApiRequest($request->input('api_token'), $request->getRequestUri());
UserRequest::addApiRequest($user->id, $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' : ''));
@@ -332,20 +316,17 @@ class ApiV2Controller extends BasePageController
public function details(Request $request): JsonResponse
{
if ($request->missing('api_token') || $request->isNotFilled('api_token')) {
return response()->json(['error' => 'Missing parameter (api_token)'], 403);
$user = $this->resolveUser($request);
if (! $user) {
return response()->json(['error' => 'Missing or invalid API key'], 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);
}
UserRequest::addApiRequest($user->id, $request->getRequestUri());
event(new UserAccessedApi($user, $request->ip()));
$relData = Release::getByGuid($request->input('id'));
$relData = Release::getByGuidForApi($request->input('id'));
$relData = fractal($relData, new DetailsTransformer($user));
+239 -3
View File
@@ -98,7 +98,9 @@ class XML_Response
$this->xml = new \XMLWriter;
$this->xml->openMemory();
$this->xml->setIndent(true);
// Disable indentation for API responses (smaller payload, faster generation).
// Clients (Sonarr, Radarr, etc.) don't need pretty-printed XML.
$this->xml->setIndent(false);
}
public function returnXML(): bool|string
@@ -121,6 +123,207 @@ class XML_Response
return false;
}
/**
* Build the API response as a PHP array instead of XML.
* Used for JSON output to avoid the expensive XML->xml_to_array->json_encode path.
*
* @return array<string, mixed>|false
*/
public function returnArray(): array|false
{
return match ($this->type) {
'caps' => $this->buildCapsArray(),
'api' => $this->buildApiArray(),
'reg' => $this->buildRegArray(),
default => false,
};
}
/**
* Build capabilities response as array.
*
* @return array<string, mixed>
*/
protected function buildCapsArray(): array
{
return [
'server' => $this->server['server'],
'limits' => $this->server['limits'],
'registration' => $this->server['registration'],
'searching' => $this->server['searching'],
'categories' => $this->server['categories'] ?? [],
];
}
/**
* Build API response as array.
*
* @return array<string, mixed>
*/
protected function buildApiArray(): array
{
$response = [
'offset' => $this->offset,
'total' => $this->releases[0]->_totalrows ?? 0,
];
$response['apilimits'] = [
'apicurrent' => $this->parameters['requests'],
'apimax' => $this->parameters['apilimit'],
'grabcurrent' => $this->parameters['grabs'],
'grabmax' => $this->parameters['downloadlimit'],
];
if (! empty($this->parameters['oldestapi'])) {
$response['apilimits']['apioldesttime'] = $this->parameters['oldestapi'];
}
if (! empty($this->parameters['oldestgrab'])) {
$response['apilimits']['graboldesttime'] = $this->parameters['oldestgrab'];
}
$response['item'] = [];
if (! empty($this->releases)) {
$releases = $this->releases instanceof Release ? [$this->releases] : $this->releases;
foreach ($releases as $release) {
$this->release = $release;
$item = $this->buildReleaseArray();
$response['item'][] = $item;
}
}
return $response;
}
/**
* Build a single release as array.
*
* @return array<string, mixed>
*/
protected function buildReleaseArray(): array
{
$serverUrl = $this->server['server']['url'];
$delParam = ((int) $this->parameters['del'] === 1 ? '&del=1' : '');
$item = [
'title' => $this->release->searchname,
'guid' => $serverUrl.'/details/'.$this->release->guid,
'link' => $serverUrl.'/getnzb?id='.$this->release->guid.'.nzb&r='.$this->parameters['token'].$delParam,
'comments' => $serverUrl.'/details/'.$this->release->guid.'#comments',
'pubDate' => date(DATE_RSS, strtotime($this->release->adddate)),
'category' => $this->release->category_name,
'description' => $this->release->searchname,
];
if (! isset($this->parameters['dl']) || (int) $this->parameters['dl'] === 1) {
$item['enclosure'] = [
'url' => $serverUrl.'/getnzb?id='.$this->release->guid.'.nzb&r='.$this->parameters['token'].$delParam,
'length' => $this->release->size,
'type' => 'application/x-nzb',
];
}
// Attributes
$attrs = [
'category' => $this->release->categories_id,
'size' => $this->release->size,
];
if (! empty($this->release->coverurl)) {
$attrs['coverurl'] = $serverUrl.'/covers/'.$this->release->coverurl;
}
if ((int) $this->parameters['extended'] === 1) {
$attrs['files'] = $this->release->totalpart;
if (($this->release->videos_id > 0 || $this->release->tv_episodes_id > 0)) {
$attrs = array_merge($attrs, $this->buildTvAttrArray());
}
if (isset($this->release->imdbid) && $this->release->imdbid > 0) {
$attrs['imdb'] = $this->release->imdbid;
}
if (isset($this->release->anidbid) && $this->release->anidbid > 0) {
$attrs['anidbid'] = $this->release->anidbid;
}
if (isset($this->release->predb_id) && $this->release->predb_id > 0) {
$attrs['prematch'] = '1';
}
if (isset($this->release->nfostatus) && (int) $this->release->nfostatus === 1) {
$attrs['info'] = $serverUrl.'api?t=info&id='.$this->release->guid.'&r='.$this->parameters['token'];
}
$attrs['grabs'] = $this->release->grabs;
$attrs['comments'] = $this->release->comments;
$attrs['password'] = $this->release->passwordstatus;
$attrs['usenetdate'] = Carbon::parse($this->release->postdate)->toRssString();
if (! empty($this->release->group_name)) {
$attrs['group'] = $this->release->group_name;
}
}
$item['attr'] = $attrs;
return $item;
}
/**
* Build TV attributes as array (scalar-safe).
*
* @return array<string, mixed>
*/
protected function buildTvAttrArray(): array
{
$attrs = [];
if (! empty($this->release->title)) {
$attrs['title'] = $this->release->title;
}
if (isset($this->release->series) && $this->release->series > 0) {
$attrs['season'] = $this->release->series;
}
$episodeNum = $this->getScalarOrRelationValue('episode', 'episode');
if (! empty($episodeNum) && $episodeNum > 0) {
$attrs['episode'] = $episodeNum;
}
if (! empty($this->release->firstaired)) {
$attrs['tvairdate'] = $this->release->firstaired;
}
if (isset($this->release->tvdb) && $this->release->tvdb > 0) {
$attrs['tvdbid'] = $this->release->tvdb;
}
if (isset($this->release->trakt) && $this->release->trakt > 0) {
$attrs['traktid'] = $this->release->trakt;
}
if (isset($this->release->tvrage) && $this->release->tvrage > 0) {
$attrs['tvrageid'] = $this->release->tvrage;
$attrs['rageid'] = $this->release->tvrage;
}
if (isset($this->release->tvmaze) && $this->release->tvmaze > 0) {
$attrs['tvmazeid'] = $this->release->tvmaze;
}
if (isset($this->release->imdb) && $this->release->imdb > 0) {
$attrs['imdbid'] = $this->release->imdb;
}
if (isset($this->release->tmdb) && $this->release->tmdb > 0) {
$attrs['tmdbid'] = $this->release->tmdb;
}
return $attrs;
}
/**
* Build registration response as array.
*
* @return array<string, mixed>
*/
protected function buildRegArray(): array
{
return [
'username' => $this->parameters['username'],
'password' => $this->parameters['password'],
'apikey' => $this->parameters['token'],
];
}
/**
* XML writes and returns the API capabilities.
*
@@ -469,6 +672,8 @@ class XML_Response
/**
* Writes the TV Specific attributes.
* Uses scalar-safe access to avoid N+1 lazy loading when release data
* comes from raw SQL queries (stdClass with flat columns) vs Eloquent models.
*/
protected function setTvAttr(): void
{
@@ -478,8 +683,10 @@ class XML_Response
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);
// episode can be a scalar (from raw SQL JOIN) or an Eloquent relation object
$episodeNum = $this->getScalarOrRelationValue('episode', 'episode');
if (! empty($episodeNum) && $episodeNum > 0) {
$this->writeZedAttr('episode', $episodeNum);
}
if (! empty($this->release->firstaired)) {
$this->writeZedAttr('tvairdate', $this->release->firstaired);
@@ -505,6 +712,35 @@ class XML_Response
}
}
/**
* Safely get a value that may be a scalar (from raw SQL) or a property on a related object.
* Prevents N+1 lazy loading when accessing Eloquent relation properties in a loop.
*
* @param string $property The property name on the release (may be scalar or object)
* @param string $subProperty The sub-property to access if $property is an object
* @return mixed The scalar value, or null if not available
*/
protected function getScalarOrRelationValue(string $property, string $subProperty): mixed
{
$value = $this->release->$property ?? null;
if ($value === null) {
return null;
}
// If it's a scalar (from raw SQL JOIN), return directly
if (is_scalar($value)) {
return $value;
}
// If it's an object (Eloquent relation), access the sub-property
if (is_object($value)) {
return $value->$subProperty ?? null;
}
return null;
}
/**
* Writes individual zed (newznab) type attributes.
*
+38
View File
@@ -426,6 +426,44 @@ class Release extends Model
return is_array($guid) ? $releases : $releases->first();
}
/**
* Lighter version of getByGuid() optimized for API details responses.
* Skips video.tvInfo, releaseGroup, and fields not used by DetailsTransformer/XML_Response.
*/
public static function getByGuidForApi(mixed $guid): mixed
{
$query = self::with([
'group:id,name',
'category:id,title,root_categories_id',
'category.parent:id,title',
'video:id,title,tvdb,trakt,tvrage,tvmaze',
'episode:id,title,firstaired',
]);
if (is_array($guid)) {
$query->whereIn('guid', $guid);
} else {
$query->where('guid', $guid);
}
$releases = $query->get();
$releases->each(function ($release) {
$release->group_name = $release->group->name ?? null;
$release->tvdb = $release->video->tvdb ?? null;
$release->trakt = $release->video->trakt ?? null;
$release->tvrage = $release->video->tvrage ?? null;
$release->tvmaze = $release->video->tvmaze ?? null;
$release->title = $release->episode->title ?? null;
$release->firstaired = $release->episode->firstaired ?? null;
$release->parent_category = $release->category->parent->title ?? null;
$release->sub_category = $release->category->title ?? null;
$release->category_name = $release->parent_category.' > '.$release->sub_category;
});
return is_array($guid) ? $releases : $releases->first();
}
/**
* Get a range of releases. used in admin manage list.
*/
+3 -4
View File
@@ -61,17 +61,16 @@ class UserDownload extends Model
/**
* Get the COUNT of how many NZB's the user has downloaded in the past day.
*
* Note: Old request cleanup is no longer done inline to avoid blocking API responses.
* Use a scheduled command to periodically clean old records instead.
*
* @throws \Exception
*/
public static function getDownloadRequests(int $userID): int
{
// Clear old requests.
self::whereUsersId($userID)->where('timestamp', '<', now()->subDay())->delete();
$value = self::whereUsersId($userID)->where('timestamp', '>', now()->subDay())->count('id');
return $value === false ? 0 : $value;
return $value ?: 0;
}
/**
+14 -8
View File
@@ -66,18 +66,20 @@ class UserRequest extends Model
/**
* Get the quantity of API requests in the last day for the users_id.
*
* Note: Old request cleanup is no longer done inline to avoid blocking API responses.
* Use clearApiRequests() via a scheduled command or queue job instead.
*
* @throws \Exception
* @throws \Throwable
*/
public static function getApiRequests(int $userID): int
{
// Clear old requests.
self::clearApiRequests($userID);
$requests = self::query()->where('users_id', $userID)->count('id');
$requests = self::query()
->where('users_id', $userID)
->where('timestamp', '>', now()->subDay())
->count('id');
return ! $requests ? 0 : $requests;
return $requests ?: 0;
}
/**
@@ -117,12 +119,16 @@ class UserRequest extends Model
/**
* If a user accesses the API, log it.
*
* @param string $token API token of the user
* @param string|int $tokenOrUserId API token string or user ID integer
* @param string $request The API request.
*/
public static function addApiRequest(string $token, string $request): void
public static function addApiRequest(string|int $tokenOrUserId, string $request): void
{
$userID = User::query()->select(['id'])->where('api_token', $token)->value('id');
if (is_int($tokenOrUserId)) {
$userID = $tokenOrUserId;
} else {
$userID = User::query()->select(['id'])->where('api_token', $tokenOrUserId)->value('id');
}
self::query()->insert(['users_id' => $userID, 'request' => $request, 'timestamp' => now()]);
}
+49 -22
View File
@@ -11,19 +11,19 @@
Here lives the documentation for the API for accessing NZB and index data. API functions can be called by either
logged in users, or by providing an API key.
</p>
@if($loggedin ?? false)
@auth
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 mb-6 border border-gray-200 dark:border-gray-700 dark:bg-gray-700 dark:border-gray-600">
<h4 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-key mr-2 text-gray-600 dark:text-gray-400"></i>Your API Credentials
</h4>
<div class="flex rounded-md shadow-sm" x-data="copyToClipboard()">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white" value="apikey={{ $userdata->api_token }}" readonly id="apikeyInput">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white" value="apikey={{ auth()->user()->api_token }}" readonly id="apikeyInput">
<button class="inline-flex items-center px-4 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500" type="button" x-on:click="copy('apikeyInput')" title="Copy to clipboard" x-bind:class="copied ? 'text-green-600' : ''">
<i class="fa" x-bind:class="copied ? 'fa-check' : 'fa-copy'"></i>
</button>
</div>
</div>
@endif
@endauth
<h4 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-plug mr-2 text-gray-600 dark:text-gray-400"></i>Available Functions
</h4>
@@ -72,16 +72,23 @@
</td>
<td class="px-6 py-4">
<div class="flex flex-col gap-2">
@if($loggedin ?? false)
<a href="{{ url('/api/v1/api?t=search&q=linux&apikey=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v1/api?t=search&q=linux&apikey=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">?t=search&amp;q=linux</code>
</a>
<a href="{{ url('/api/v1/api?t=search&cat=' . $catClass::GAME_ROOT . ',' . $catClass::MOVIE_ROOT . '&apikey=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<a href="{{ url('/api/v1/api?t=search&cat=' . $catClass::GAME_ROOT . ',' . $catClass::MOVIE_ROOT . '&apikey=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">?t=search&amp;cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }}</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>?t=search&amp;q=linux</code>
</span>
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>?t=search&amp;cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }}</code>
</span>
@endauth
</div>
</td>
</tr>
@@ -104,12 +111,16 @@
</div>
</td>
<td class="px-6 py-4">
@if($loggedin ?? false)
<a href="{{ url('/api/v1/api?t=tvsearch&q=law%20and%20order&season=7&ep=12&apikey=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v1/api?t=tvsearch&q=law%20and%20order&season=7&ep=12&apikey=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">?t=tvsearch&amp;q=law and order&amp;season=7&amp;ep=12</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>?t=tvsearch&amp;q=law and order&amp;season=7&amp;ep=12</code>
</span>
@endauth
</td>
</tr>
<tr class="hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700">
@@ -126,24 +137,32 @@
</div>
</td>
<td class="px-6 py-4">
@if($loggedin ?? false)
<a href="{{ url('/api/v1/api?t=movie&imdbid=1418646&apikey=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v1/api?t=movie&imdbid=1418646&apikey=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">?t=movie&amp;imdbid=1418646</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>?t=movie&amp;imdbid=1418646</code>
</span>
@endauth
</td>
</tr>
<tr class="hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700">
<td class="px-6 py-4"><strong class="text-gray-900 dark:text-gray-100 dark:text-white">Details</strong></td>
<td class="px-6 py-4"><span class="text-gray-700 dark:text-gray-300">Returns detailed information about an NZB.</span></td>
<td class="px-6 py-4">
@if($loggedin ?? false)
<a href="{{ url('/api/v1/api?t=details&id=9ca52909ba9b9e5e6758d815fef4ecda&apikey=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v1/api?t=details&id=9ca52909ba9b9e5e6758d815fef4ecda&apikey=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">?t=details&amp;id=9ca52909ba9b9e5e6758d815fef4ecda</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>?t=details&amp;id=&lt;guid&gt;</code>
</span>
@endauth
</td>
</tr>
<tr class="hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700">
@@ -154,24 +173,32 @@
</span>
</td>
<td class="px-6 py-4">
@if($loggedin ?? false)
<a href="{{ url('/api/v1/api?t=info&id=9ca52909ba9b9e5e6758d815fef4ecda&apikey=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v1/api?t=info&id=9ca52909ba9b9e5e6758d815fef4ecda&apikey=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">?t=info&amp;id=9ca52909ba9b9e5e6758d815fef4ecda</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>?t=info&amp;id=&lt;guid&gt;</code>
</span>
@endauth
</td>
</tr>
<tr class="hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700">
<td class="px-6 py-4"><strong class="text-gray-900 dark:text-gray-100 dark:text-white">Get</strong></td>
<td class="px-6 py-4"><span class="text-gray-700 dark:text-gray-300">Downloads the NZB file associated with an ID.</span></td>
<td class="px-6 py-4">
@if($loggedin ?? false)
<a href="{{ url('/api/v1/api?t=get&id=9ca52909ba9b9e5e6758d815fef4ecda&apikey=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v1/api?t=get&id=9ca52909ba9b9e5e6758d815fef4ecda&apikey=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">?t=get&amp;id=9ca52909ba9b9e5e6758d815fef4ecda</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>?t=get&amp;id=&lt;guid&gt;</code>
</span>
@endauth
</td>
</tr>
</tbody>
+50 -22
View File
@@ -12,19 +12,19 @@
Here lives the documentation for the API v2 for accessing NZB and index data. API functions can be called by providing an API token.
</p>
@if($loggedin ?? false)
@auth
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 mb-6 border border-gray-200 dark:border-gray-700 dark:bg-gray-700 dark:border-gray-600">
<h4 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-key mr-2 text-gray-600 dark:text-gray-400"></i>Your API Credentials
</h4>
<div class="flex rounded-md shadow-sm" x-data="copyToClipboard()">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white" value="api_token={{ $userdata->api_token }}" readonly id="apikeyInput">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white" value="api_token={{ auth()->user()->api_token }}" readonly id="apikeyInput">
<button class="inline-flex items-center px-4 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500" type="button" x-on:click="copy('apikeyInput')" title="Copy to clipboard" x-bind:class="copied ? 'text-green-600' : ''">
<i class="fa" x-bind:class="copied ? 'fa-check' : 'fa-copy'"></i>
</button>
</div>
</div>
@endif
@endauth
<h4 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-plug mr-2 text-gray-600 dark:text-gray-400"></i>Available Functions
@@ -68,16 +68,23 @@
</td>
<td class="px-6 py-4">
<div class="flex flex-col gap-2">
@if($loggedin ?? false)
<a href="{{ url('/api/v2/search?id=linux&api_token=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v2/search?id=linux&api_token=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">search?id=linux</code>
</a>
<a href="{{ url('/api/v2/search?cat=' . $catClass::GAME_ROOT . ',' . $catClass::MOVIE_ROOT . '&api_token=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<a href="{{ url('/api/v2/search?cat=' . $catClass::GAME_ROOT . ',' . $catClass::MOVIE_ROOT . '&api_token=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">search?cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }}</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>search?id=linux</code>
</span>
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>search?cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }}</code>
</span>
@endauth
</div>
</td>
</tr>
@@ -101,16 +108,23 @@
</td>
<td class="px-6 py-4">
<div class="flex flex-col gap-2">
@if($loggedin ?? false)
<a href="{{ url('/api/v2/tv?id=law%20and%20order&season=7&ep=12&api_token=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v2/tv?id=law%20and%20order&season=7&ep=12&api_token=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">tv?id=law and order&amp;season=7&amp;ep=12</code>
</a>
<a href="{{ url('/api/v2/tv?rid=2204&cat=' . $catClass::GAME_ROOT . ',' . $catClass::MOVIE_ROOT . '&api_token=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<a href="{{ url('/api/v2/tv?rid=2204&cat=' . $catClass::GAME_ROOT . ',' . $catClass::MOVIE_ROOT . '&api_token=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">tv?rid=2204&amp;cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }}</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>tv?id=law and order&amp;season=7&amp;ep=12</code>
</span>
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>tv?rid=2204&amp;cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }}</code>
</span>
@endauth
</div>
</td>
</tr>
@@ -131,16 +145,23 @@
</td>
<td class="px-6 py-4">
<div class="flex flex-col gap-2">
@if($loggedin ?? false)
<a href="{{ url('/api/v2/movies?imdbid=1418646&api_token=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v2/movies?imdbid=1418646&api_token=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">movies?imdbid=1418646</code>
</a>
<a href="{{ url('/api/v2/movies?imdbid=1418646&cat=' . $catClass::MOVIE_SD . ',' . $catClass::MOVIE_HD . '&api_token=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<a href="{{ url('/api/v2/movies?imdbid=1418646&cat=' . $catClass::MOVIE_SD . ',' . $catClass::MOVIE_HD . '&api_token=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">movies?imdbid=1418646&amp;cat={{ $catClass::MOVIE_SD }},{{ $catClass::MOVIE_HD }}</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>movies?imdbid=1418646</code>
</span>
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>movies?imdbid=1418646&amp;cat={{ $catClass::MOVIE_SD }},{{ $catClass::MOVIE_HD }}</code>
</span>
@endauth
</div>
</td>
</tr>
@@ -148,24 +169,32 @@
<td class="px-6 py-4"><strong class="text-gray-900 dark:text-gray-100 dark:text-white">Details</strong></td>
<td class="px-6 py-4"><span class="text-gray-700 dark:text-gray-300">Returns detailed information about an NZB.</span></td>
<td class="px-6 py-4">
@if($loggedin ?? false)
<a href="{{ url('/api/v2/details?id=9ca52909ba9b9e5e6758d815fef4ecda&api_token=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v2/details?id=9ca52909ba9b9e5e6758d815fef4ecda&api_token=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">details?id=9ca52909ba9b9e5e6758d815fef4ecda</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>details?id=&lt;guid&gt;</code>
</span>
@endauth
</td>
</tr>
<tr class="hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700">
<td class="px-6 py-4"><strong class="text-gray-900 dark:text-gray-100 dark:text-white">Get NZB</strong></td>
<td class="px-6 py-4"><span class="text-gray-700 dark:text-gray-300">Downloads the NZB file associated with an ID.</span></td>
<td class="px-6 py-4">
@if($loggedin ?? false)
<a href="{{ url('/api/v2/getnzb?id=9ca52909ba9b9e5e6758d815fef4ecda&api_token=' . $userdata->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
@auth
<a href="{{ url('/api/v2/getnzb?id=9ca52909ba9b9e5e6758d815fef4ecda&api_token=' . auth()->user()->api_token) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600">
<i class="fa fa-external-link-alt mr-1"></i>
<code class="text-blue-700 dark:text-blue-400">getnzb?id=9ca52909ba9b9e5e6758d815fef4ecda</code>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700">
<code>getnzb?id=&lt;guid&gt;</code>
</span>
@endauth
</td>
</tr>
</tbody>
@@ -194,4 +223,3 @@
</div>
</div>
@endsection
+20 -20
View File
@@ -13,19 +13,19 @@
direct NZB downloads based on your preferences.
</p>
@if($loggedin ?? false)
@auth
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 mb-6 border border-gray-200 dark:border-gray-700 dark:bg-gray-700 dark:border-gray-600">
<h4 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-key mr-2 text-gray-600 dark:text-gray-400"></i>Your API Token
</h4>
<div class="flex rounded-md shadow-sm">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white" value="api_token={{ $userdata->api_token }}" readonly id="apiTokenInput">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white" value="api_token={{ auth()->user()->api_token }}" readonly id="apiTokenInput">
<button class="inline-flex items-center px-4 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500" type="button" id="copyApiToken" title="Copy to clipboard">
<i class="fa fa-copy"></i>
</button>
</div>
</div>
@endif
@endauth
<h4 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-cog mr-2 text-gray-600 dark:text-gray-400"></i>RSS Configuration Options
@@ -43,7 +43,7 @@
<tr class="hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700">
<td class="px-6 py-4"><code class="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-sm text-red-600 dark:bg-gray-700 dark:text-red-400">api_token</code></td>
<td class="px-6 py-4 text-gray-700 dark:text-gray-300">Add this to your feed URL to allow NZB downloads without logging in</td>
<td class="px-6 py-4"><code class="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-sm text-red-600 dark:bg-gray-700 dark:text-red-400">&amp;api_token={{ $userdata->api_token ?? 'YOUR_TOKEN' }}</code></td>
<td class="px-6 py-4"><code class="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-sm text-red-600 dark:bg-gray-700 dark:text-red-400">&amp;api_token={{ auth()->user()->api_token ?? 'YOUR_TOKEN' }}</code></td>
</tr>
<tr class="hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700">
<td class="px-6 py-4"><code class="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-sm text-red-600 dark:bg-gray-700 dark:text-red-400">del=1</code></td>
@@ -99,12 +99,12 @@
<strong class="text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-rss mr-2 text-orange-500 dark:text-orange-400"></i>Full Site Feed
</strong>
<a href="{{ url('/rss/full-feed?dl=1&api_token=' . ($userdata->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<a href="{{ url('/rss/full-feed?dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<i class="fa fa-external-link mr-1"></i>Open Feed
</a>
</div>
<div class="flex rounded-md shadow-sm">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/full-feed?dl=1&api_token=' . ($userdata->api_token ?? '')) }}" readonly id="fullFeedUrl">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/full-feed?dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" readonly id="fullFeedUrl">
<button class="inline-flex items-center px-3 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500 copy-btn" type="button" data-copy-target="fullFeedUrl">
<i class="fa fa-copy"></i>
</button>
@@ -117,12 +117,12 @@
<strong class="text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-shopping-basket mr-2 text-blue-500 dark:text-blue-400"></i>My Cart Feed
</strong>
<a href="{{ url('/rss/cart?dl=1&api_token=' . ($userdata->api_token ?? '') . '&del=1') }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<a href="{{ url('/rss/cart?dl=1&api_token=' . (auth()->user()->api_token ?? '') . '&del=1') }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<i class="fa fa-external-link mr-1"></i>Open Feed
</a>
</div>
<div class="flex rounded-md shadow-sm">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/cart?dl=1&api_token=' . ($userdata->api_token ?? '') . '&del=1') }}" readonly id="cartFeedUrl">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/cart?dl=1&api_token=' . (auth()->user()->api_token ?? '') . '&del=1') }}" readonly id="cartFeedUrl">
<button class="inline-flex items-center px-3 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500 copy-btn" type="button" data-copy-target="cartFeedUrl">
<i class="fa fa-copy"></i>
</button>
@@ -134,12 +134,12 @@
<strong class="text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-tv mr-2 text-green-600 dark:text-green-400"></i>My Shows Feed
</strong>
<a href="{{ url('/rss/myshows?dl=1&api_token=' . ($userdata->api_token ?? '') . '&del=1') }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<a href="{{ url('/rss/myshows?dl=1&api_token=' . (auth()->user()->api_token ?? '') . '&del=1') }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<i class="fa fa-external-link mr-1"></i>Open Feed
</a>
</div>
<div class="flex rounded-md shadow-sm">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/myshows?dl=1&api_token=' . ($userdata->api_token ?? '') . '&del=1') }}" readonly id="myShowsFeedUrl">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/myshows?dl=1&api_token=' . (auth()->user()->api_token ?? '') . '&del=1') }}" readonly id="myShowsFeedUrl">
<button class="inline-flex items-center px-3 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500 copy-btn" type="button" data-copy-target="myShowsFeedUrl">
<i class="fa fa-copy"></i>
</button>
@@ -151,12 +151,12 @@
<strong class="text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-film mr-2 text-red-600 dark:text-red-400"></i>My Movies Feed
</strong>
<a href="{{ url('/rss/mymovies?dl=1&api_token=' . ($userdata->api_token ?? '') . '&del=1') }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<a href="{{ url('/rss/mymovies?dl=1&api_token=' . (auth()->user()->api_token ?? '') . '&del=1') }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<i class="fa fa-external-link mr-1"></i>Open Feed
</a>
</div>
<div class="flex rounded-md shadow-sm">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/mymovies?dl=1&api_token=' . ($userdata->api_token ?? '') . '&del=1') }}" readonly id="myMoviesFeedUrl">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/mymovies?dl=1&api_token=' . (auth()->user()->api_token ?? '') . '&del=1') }}" readonly id="myMoviesFeedUrl">
<button class="inline-flex items-center px-3 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500 copy-btn" type="button" data-copy-target="myMoviesFeedUrl">
<i class="fa fa-copy"></i>
</button>
@@ -179,12 +179,12 @@
<strong class="text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-film mr-2 text-blue-600 dark:text-blue-400"></i>Trending Movies
</strong>
<a href="{{ url('/rss/trending-movies?dl=1&api_token=' . ($userdata->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<a href="{{ url('/rss/trending-movies?dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<i class="fa fa-external-link mr-1"></i>Open Feed
</a>
</div>
<div class="flex rounded-md shadow-sm">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/trending-movies?dl=1&api_token=' . ($userdata->api_token ?? '')) }}" readonly id="trendingMoviesFeedUrl">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/trending-movies?dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" readonly id="trendingMoviesFeedUrl">
<button class="inline-flex items-center px-3 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500 copy-btn" type="button" data-copy-target="trendingMoviesFeedUrl">
<i class="fa fa-copy"></i>
</button>
@@ -195,12 +195,12 @@
<strong class="text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-tv mr-2 text-purple-600 dark:text-purple-400"></i>Trending TV Shows
</strong>
<a href="{{ url('/rss/trending-shows?dl=1&api_token=' . ($userdata->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<a href="{{ url('/rss/trending-shows?dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<i class="fa fa-external-link mr-1"></i>Open Feed
</a>
</div>
<div class="flex rounded-md shadow-sm">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/trending-shows?dl=1&api_token=' . ($userdata->api_token ?? '')) }}" readonly id="trendingShowsFeedUrl">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/trending-shows?dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" readonly id="trendingShowsFeedUrl">
<button class="inline-flex items-center px-3 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500 copy-btn" type="button" data-copy-target="trendingShowsFeedUrl">
<i class="fa fa-copy"></i>
</button>
@@ -224,12 +224,12 @@
<strong class="text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-folder-open mr-2 text-yellow-600 dark:text-yellow-400"></i>{{ $category['title'] }}
</strong>
<a href="{{ url('/rss/category?id=' . $category['id'] . '&dl=1&api_token=' . ($userdata->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<a href="{{ url('/rss/category?id=' . $category['id'] . '&dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<i class="fa fa-external-link mr-1"></i>Open Feed
</a>
</div>
<div class="flex rounded-md shadow-sm">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/category?id=' . $category['id'] . '&dl=1&api_token=' . ($userdata->api_token ?? '')) }}" readonly id="parentCat{{ $category['id'] }}Url">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/category?id=' . $category['id'] . '&dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" readonly id="parentCat{{ $category['id'] }}Url">
<button class="inline-flex items-center px-3 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500 copy-btn" type="button" data-copy-target="parentCat{{ $category['id'] }}Url">
<i class="fa fa-copy"></i>
</button>
@@ -258,12 +258,12 @@
<strong class="text-gray-900 dark:text-gray-100 dark:text-white flex items-center">
<i class="fa fa-tag mr-2 text-blue-500 dark:text-blue-400"></i>{{ $category['title'] }}
</strong>
<a href="{{ url('/rss/category?id=' . $category['id'] . '&dl=1&api_token=' . ($userdata->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<a href="{{ url('/rss/category?id=' . $category['id'] . '&dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" class="inline-flex items-center px-3 py-1.5 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-white dark:bg-gray-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-gray-600" target="_blank">
<i class="fa fa-external-link mr-1"></i>Open Feed
</a>
</div>
<div class="flex rounded-md shadow-sm">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/category?id=' . $category['id'] . '&dl=1&api_token=' . ($userdata->api_token ?? '')) }}" readonly id="subCat{{ $category['id'] }}Url">
<input type="text" class="flex-1 rounded-l-md border-gray-300 dark:border-gray-600 font-mono text-xs focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" value="{{ url('/rss/category?id=' . $category['id'] . '&dl=1&api_token=' . (auth()->user()->api_token ?? '')) }}" readonly id="subCat{{ $category['id'] }}Url">
<button class="inline-flex items-center px-3 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-500 copy-btn" type="button" data-copy-target="subCat{{ $category['id'] }}Url">
<i class="fa fa-copy"></i>
</button>
+5
View File
@@ -49,5 +49,10 @@ if (config('nntmux.purge_inactive_users') === true) {
Schedule::job(new RemoveInactiveAccounts)->daily();
Schedule::job(new PurgeDeletedAccounts)->daily();
}
// Cleanup old API requests and download logs (older than 1 day) - deferred from inline API calls
Schedule::call(function () {
\App\Models\UserRequest::clearApiRequests(false);
\App\Models\UserDownload::where('timestamp', '<', now()->subDay())->delete();
})->name('cleanup-api-request-logs')->hourly()->withoutOverlapping();
// Check tmux health and auto-restart if monitor pane is dead
Schedule::command('tmux:health-check --auto-restart')->everyThirtyMinutes()->withoutOverlapping();