mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Improvew API response
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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&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&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&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&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&q=law and order&season=7&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&q=law and order&season=7&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&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&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&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&id=<guid></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&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&id=<guid></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&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&id=<guid></code>
|
||||
</span>
|
||||
@endauth
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -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&season=7&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&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&season=7&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&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&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&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=<guid></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=<guid></code>
|
||||
</span>
|
||||
@endauth
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -194,4 +223,3 @@
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -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">&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">&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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user