diff --git a/Blacklight/libraries/TraktAPI.php b/Blacklight/libraries/TraktAPI.php deleted file mode 100755 index 5005214fd..000000000 --- a/Blacklight/libraries/TraktAPI.php +++ /dev/null @@ -1,223 +0,0 @@ -requestHeaders = $headers; - - $this->client = new Client; - } - - /** - * @throws GuzzleException - */ - public function episodeSummary(int $id, string $season = '', string $ep = '', string $type = 'min'): bool|array - { - $extended = match ($type) { - 'aliases', 'full', 'full,aliases' => $type, - default => 'min', - }; - - $url = self::API_URL."shows/{$id}/seasons/{$season}/episodes/{$ep}"; - - $array = $this->getJsonArray($url, $extended); - if (! \is_array($array)) { - return false; - } - - return $array; - } - - /** - * @throws GuzzleException+ - */ - public function getBoxOffice(): bool|array - { - $array = $this->getJsonArray( - self::API_URL.'movies/boxoffice' - ); - if (! $array) { - return false; - } - - return $array; - } - - /** - * @throws GuzzleException - */ - public function getCalendar(string $start = '', int $days = 7): bool|array - { - $array = $this->getJsonArray( - self::API_URL.'calendars/all/shows/'.$start.'/'.$days - ); - if (! $array) { - return false; - } - - return $array; - } - - /** - * @return array|false - * - * @throws GuzzleException - */ - private function getJsonArray(string $URI, string $extended = 'min'): bool|array - { - if ($extended === '') { - $extendedString = ''; - } else { - $extendedString = '?extended='.$extended; - } - - if (! empty($this->requestHeaders)) { - try { - $json = $this->client->get( - $URI.$extendedString, - [ - 'headers' => $this->requestHeaders, - ] - )->getBody()->getContents(); - } catch (RequestException $e) { - return false; - } catch (\RuntimeException $e) { - return false; - } - - if (! empty($json)) { - $json = json_decode($json, true); - if (! \is_array($json) || (isset($json['status']) && $json['status'] === 'failure')) { - return false; - } - - return $json; - } - } - - return false; - } - - /** - * @return array|bool|mixed - * - * @throws GuzzleException - */ - public function movieSummary(string $movie = '', string $type = 'imdbid'): mixed - { - $extended = match ($type) { - 'full' => $type, - default => 'min', - }; - $array = $this->getJsonArray(self::API_URL.'movies/'.Str::slug($movie), $extended); - if (! $array) { - return false; - } - if ($type === 'imdbid' && isset($array['ids']['imdb'])) { - return $array['ids']['imdb']; - } - - return $array; - } - - /** - * @return array|bool|void - * - * @throws GuzzleException - */ - public function searchId(int|string $id, string $site = 'trakt', int|string $type = 0) - { - if (! \in_array($site, self::$types, false) || ! ctype_digit($id)) { - return; - } - if ($site === 'imdb') { - $id = 'tt'.$id; - } - - switch (true) { - case $site === 'trakt' && ($type === 0 || $type === 2): - $type = $site.'-show'; - break; - case $site === 'trakt' && $type === 1: - $type = $site.'-movie'; - break; - case $site === 'trakt' && $type === -1: - $type = $site.'-episode'; - break; - default: - } - - $url = self::API_URL.'search?id_type='.$type.'&id='.$id; - - return $this->getJsonArray($url, ''); - } - - /** - * @throws GuzzleException - */ - public function showSearch(string $show = '', string $type = 'show'): bool|array - { - $searchUrl = self::API_URL.'search?query='. - Str::slug($show). - '&type='.$type; - - return $this->getJsonArray($searchUrl, ''); - } - - /** - * @return array|bool|void - * - * @throws GuzzleException - */ - public function showSummary(string $show = '', string $type = 'full') - { - if (empty($show)) { - return; - } - $showUrl = self::API_URL.'shows/'.Str::slug($show); - - if ($type === 'full') { - $extended = 'full'; - } else { - $extended = ''; - } - - return $this->getJsonArray($showUrl, $extended); - } -} diff --git a/Blacklight/processing/tv/TraktTv.php b/Blacklight/processing/tv/TraktTv.php index c86552777..a92336b65 100755 --- a/Blacklight/processing/tv/TraktTv.php +++ b/Blacklight/processing/tv/TraktTv.php @@ -2,9 +2,8 @@ namespace Blacklight\processing\tv; -use Blacklight\libraries\TraktAPI; +use App\Services\TraktService; use Blacklight\ReleaseImage; -use GuzzleHttp\Exception\GuzzleException; use Illuminate\Support\Carbon; /** @@ -16,7 +15,7 @@ class TraktTv extends TV { private const MATCH_PROBABILITY = 75; - public TraktAPI $client; + public TraktService $client; public $time; @@ -41,14 +40,7 @@ class TraktTv extends TV public function __construct() { parent::__construct(); - $clientId = config('nntmux_api.trakttv_api_key'); - $requestHeaders = [ - 'Content-Type' => 'application/json', - 'trakt-api-version' => 2, - 'trakt-api-key' => $clientId, - 'Content-Length' => 0, - ]; - $this->client = new TraktAPI($requestHeaders); + $this->client = new TraktService(); } /** @@ -245,13 +237,13 @@ class TraktTv extends TV } /** - * @throws GuzzleException + * Get episode information from Trakt. */ public function getEpisodeInfo(int|string $siteId, int|string $series, int|string $episode): array|bool { $return = false; - $response = $this->client->episodeSummary($siteId, $series, $episode); + $response = $this->client->getEpisodeSummary($siteId, $series, $episode); sleep(1); @@ -295,9 +287,9 @@ class TraktTv extends TV } /** - * @return array|false + * Get show information from Trakt by name. * - * @throws GuzzleException + * @return array|false */ public function getShowInfo(string $name): array|bool { @@ -309,7 +301,7 @@ class TraktTv extends TV // Do this for the API Search only as a local lookup should require it $name = preg_replace('# \((19|20)\d{2}\)$#', '', $name); - $response = (array) $this->client->showSearch($name); + $response = (array) $this->client->searchShows($name); sleep(1); @@ -333,7 +325,7 @@ class TraktTv extends TV } } if ($highest !== null) { - $fullShow = $this->client->showSummary($highest['show']['ids']['trakt']); + $fullShow = $this->client->getShowSummary($highest['show']['ids']['trakt']); if ($this->checkRequiredAttr($fullShow, 'traktS')) { $return = $this->formatShowInfo($fullShow); } diff --git a/app/Services/TraktService.php b/app/Services/TraktService.php new file mode 100644 index 000000000..3489eaac2 --- /dev/null +++ b/app/Services/TraktService.php @@ -0,0 +1,442 @@ +clientId = $clientId ?? (string) config('nntmux_api.trakttv_api_key', ''); + $this->timeout = (int) config('nntmux_api.trakttv_timeout', 30); + $this->retryTimes = (int) config('nntmux_api.trakttv_retry_times', 3); + $this->retryDelay = (int) config('nntmux_api.trakttv_retry_delay', 100); + } + + /** + * Check if the API key is configured. + */ + public function isConfigured(): bool + { + return ! empty($this->clientId); + } + + /** + * Get the request headers for Trakt API. + */ + protected function getHeaders(): array + { + return [ + 'Content-Type' => 'application/json', + 'trakt-api-version' => self::API_VERSION, + 'trakt-api-key' => $this->clientId, + ]; + } + + /** + * Make a GET request to the Trakt API. + * + * @param string $endpoint The API endpoint + * @param array $params Query parameters + * @return array|null Response data or null on failure + */ + protected function get(string $endpoint, array $params = []): ?array + { + if (! $this->isConfigured()) { + Log::debug('Trakt API key is not configured'); + + return null; + } + + $url = self::BASE_URL.'/'.ltrim($endpoint, '/'); + + try { + $response = Http::timeout($this->timeout) + ->retry($this->retryTimes, $this->retryDelay) + ->withHeaders($this->getHeaders()) + ->get($url, $params); + + if ($response->successful()) { + $data = $response->json(); + + // Check for API error responses + if (isset($data['status']) && $data['status'] === 'failure') { + Log::debug('Trakt API returned failure status', [ + 'endpoint' => $endpoint, + ]); + + return null; + } + + return $data; + } + + // Handle specific error codes + if ($response->status() === 404) { + Log::debug('Trakt: Resource not found', ['endpoint' => $endpoint]); + + return null; + } + + Log::warning('Trakt API request failed', [ + 'endpoint' => $endpoint, + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + return null; + } catch (\Throwable $e) { + Log::warning('Trakt API request exception', [ + 'endpoint' => $endpoint, + 'message' => $e->getMessage(), + ]); + + return null; + } + } + + /** + * Get episode summary. + * + * @param int|string $showId The show ID (Trakt, IMDB, TMDB, or TVDB) + * @param int|string $season The season number + * @param int|string $episode The episode number + * @param string $extended Extended info level: 'min', 'full', 'aliases', 'full,aliases' + * @return array|null Episode data or null on failure + */ + public function getEpisodeSummary( + int|string $showId, + int|string $season, + int|string $episode, + string $extended = 'min' + ): ?array { + $extended = match ($extended) { + 'aliases', 'full', 'full,aliases' => $extended, + default => 'min', + }; + + $cacheKey = "trakt_episode_{$showId}_{$season}_{$episode}_{$extended}"; + + return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($showId, $season, $episode, $extended) { + return $this->get("shows/{$showId}/seasons/{$season}/episodes/{$episode}", [ + 'extended' => $extended, + ]); + }); + } + + /** + * Get current box office movies. + * + * @return array|null Box office data or null on failure + */ + public function getBoxOffice(): ?array + { + $cacheKey = 'trakt_boxoffice_'.date('Y-m-d'); + + return Cache::remember($cacheKey, now()->addHours(6), function () { + return $this->get('movies/boxoffice'); + }); + } + + /** + * Get TV show calendar. + * + * @param string $startDate Start date (YYYY-MM-DD format, defaults to today) + * @param int $days Number of days to retrieve (default: 7) + * @return array|null Calendar data or null on failure + */ + public function getCalendar(string $startDate = '', int $days = 7): ?array + { + if (empty($startDate)) { + $startDate = date('Y-m-d'); + } + + $cacheKey = "trakt_calendar_{$startDate}_{$days}"; + + return Cache::remember($cacheKey, now()->addHours(6), function () use ($startDate, $days) { + return $this->get("calendars/all/shows/{$startDate}/{$days}"); + }); + } + + /** + * Get movie summary. + * + * @param string $movie Movie slug or ID + * @param string $extended Extended info level: 'min' or 'full' + * @return array|null Movie data or null on failure + */ + public function getMovieSummary(string $movie, string $extended = 'min'): ?array + { + if (empty($movie)) { + return null; + } + + $extended = $extended === 'full' ? 'full' : 'min'; + $slug = Str::slug($movie); + $cacheKey = "trakt_movie_{$slug}_{$extended}"; + + return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($slug, $extended) { + return $this->get("movies/{$slug}", ['extended' => $extended]); + }); + } + + /** + * Get IMDB ID for a movie. + * + * @param string $movie Movie slug or ID + * @return string|null IMDB ID or null on failure + */ + public function getMovieImdbId(string $movie): ?string + { + $data = $this->getMovieSummary($movie, 'min'); + + return $data['ids']['imdb'] ?? null; + } + + /** + * Search by external ID. + * + * @param int|string $id The ID to search for + * @param string $idType The ID type: 'imdb', 'tmdb', 'trakt', 'tvdb' + * @param string $mediaType Media type: 'movie', 'show', 'episode', or empty for all + * @return array|null Search results or null on failure + */ + public function searchById(int|string $id, string $idType = 'trakt', string $mediaType = ''): ?array + { + if (! in_array($idType, self::ID_TYPES, true)) { + Log::warning('Trakt: Invalid ID type', ['idType' => $idType]); + + return null; + } + + // Format IMDB ID with 'tt' prefix if needed + if ($idType === 'imdb' && is_numeric($id)) { + $id = 'tt'.$id; + } + + $params = [ + 'id_type' => $idType, + 'id' => $id, + ]; + + if (! empty($mediaType)) { + $params['type'] = $mediaType; + } + + // Don't cache ID searches as they're typically one-off lookups + return $this->get('search/'.$idType.'/'.$id, ['type' => $mediaType ?: null]); + } + + /** + * Search for a show by name. + * + * @param string $query Search query + * @param string $type Search type: 'show', 'movie', 'episode', 'person', 'list' + * @return array|null Search results or null on failure + */ + public function searchShows(string $query, string $type = 'show'): ?array + { + if (empty($query)) { + return null; + } + + $slug = Str::slug($query); + $cacheKey = "trakt_search_{$type}_{$slug}"; + + return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($query, $type) { + return $this->get('search/'.$type, ['query' => $query]); + }); + } + + /** + * Get show summary. + * + * @param string $show Show slug or ID + * @param string $extended Extended info level: 'min' or 'full' + * @return array|null Show data or null on failure + */ + public function getShowSummary(string $show, string $extended = 'full'): ?array + { + if (empty($show)) { + return null; + } + + $extended = $extended === 'full' ? 'full' : 'min'; + $slug = Str::slug($show); + $cacheKey = "trakt_show_{$slug}_{$extended}"; + + return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($slug, $extended) { + return $this->get("shows/{$slug}", ['extended' => $extended]); + }); + } + + /** + * Get show seasons. + * + * @param string $show Show slug or ID + * @param string $extended Extended info level + * @return array|null Seasons data or null on failure + */ + public function getShowSeasons(string $show, string $extended = 'full'): ?array + { + if (empty($show)) { + return null; + } + + $slug = Str::slug($show); + $cacheKey = "trakt_seasons_{$slug}_{$extended}"; + + return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($slug, $extended) { + return $this->get("shows/{$slug}/seasons", ['extended' => $extended]); + }); + } + + /** + * Get season episodes. + * + * @param string $show Show slug or ID + * @param int $season Season number + * @param string $extended Extended info level + * @return array|null Episodes data or null on failure + */ + public function getSeasonEpisodes(string $show, int $season, string $extended = 'full'): ?array + { + if (empty($show)) { + return null; + } + + $slug = Str::slug($show); + $cacheKey = "trakt_season_episodes_{$slug}_{$season}_{$extended}"; + + return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($slug, $season, $extended) { + return $this->get("shows/{$slug}/seasons/{$season}", ['extended' => $extended]); + }); + } + + /** + * Get trending shows. + * + * @param int $limit Number of results to return + * @param string $extended Extended info level + * @return array|null Trending shows or null on failure + */ + public function getTrendingShows(int $limit = 10, string $extended = 'full'): ?array + { + $cacheKey = "trakt_trending_shows_{$limit}_{$extended}"; + + return Cache::remember($cacheKey, now()->addHours(1), function () use ($limit, $extended) { + return $this->get('shows/trending', [ + 'limit' => $limit, + 'extended' => $extended, + ]); + }); + } + + /** + * Get trending movies. + * + * @param int $limit Number of results to return + * @param string $extended Extended info level + * @return array|null Trending movies or null on failure + */ + public function getTrendingMovies(int $limit = 10, string $extended = 'full'): ?array + { + $cacheKey = "trakt_trending_movies_{$limit}_{$extended}"; + + return Cache::remember($cacheKey, now()->addHours(1), function () use ($limit, $extended) { + return $this->get('movies/trending', [ + 'limit' => $limit, + 'extended' => $extended, + ]); + }); + } + + /** + * Get popular shows. + * + * @param int $limit Number of results to return + * @param string $extended Extended info level + * @return array|null Popular shows or null on failure + */ + public function getPopularShows(int $limit = 10, string $extended = 'full'): ?array + { + $cacheKey = "trakt_popular_shows_{$limit}_{$extended}"; + + return Cache::remember($cacheKey, now()->addHours(6), function () use ($limit, $extended) { + return $this->get('shows/popular', [ + 'limit' => $limit, + 'extended' => $extended, + ]); + }); + } + + /** + * Get popular movies. + * + * @param int $limit Number of results to return + * @param string $extended Extended info level + * @return array|null Popular movies or null on failure + */ + public function getPopularMovies(int $limit = 10, string $extended = 'full'): ?array + { + $cacheKey = "trakt_popular_movies_{$limit}_{$extended}"; + + return Cache::remember($cacheKey, now()->addHours(6), function () use ($limit, $extended) { + return $this->get('movies/popular', [ + 'limit' => $limit, + 'extended' => $extended, + ]); + }); + } + + /** + * Clear cached data for a specific show. + */ + public function clearShowCache(string $show): void + { + $slug = Str::slug($show); + Cache::forget("trakt_show_{$slug}_min"); + Cache::forget("trakt_show_{$slug}_full"); + Cache::forget("trakt_seasons_{$slug}_full"); + Cache::forget("trakt_seasons_{$slug}_min"); + } + + /** + * Clear cached data for a specific movie. + */ + public function clearMovieCache(string $movie): void + { + $slug = Str::slug($movie); + Cache::forget("trakt_movie_{$slug}_min"); + Cache::forget("trakt_movie_{$slug}_full"); + } +} + diff --git a/misc/testing/Tests/test_trakt_API.php b/misc/testing/Tests/test_trakt_API.php index 5b5fe20f1..ab941f255 100755 --- a/misc/testing/Tests/test_trakt_API.php +++ b/misc/testing/Tests/test_trakt_API.php @@ -2,22 +2,22 @@ require_once dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'bootstrap/autoload.php'; -use Blacklight\processing\tv\TraktTv; +use App\Services\TraktService; $c = new Blacklight\ColorCLI; -$trakt = new TraktTv; +$trakt = new TraktService; if (! empty($argv[1]) && is_numeric($argv[2]) && is_numeric($argv[3])) { // Test if your Trakt API key and configuration are working // If it works you should get a printed array of the show/season/episode entered // Search for a show - $series = $trakt->client->showSearch((string) $argv[1], 'show'); + $series = $trakt->searchShows((string) $argv[1], 'show'); // Use the first show found (highest match) and get the requested season/episode from $argv if (is_array($series)) { - $series = $trakt->client->showSummary($series[0]['show']['ids']['trakt'], 'full,images'); - $episode = $trakt->client->episodeSummary($series['ids']['trakt'], (int) $argv[2], (int) $argv[3], 'full'); + $series = $trakt->getShowSummary($series[0]['show']['ids']['trakt'], 'full'); + $episode = $trakt->getEpisodeSummary($series['ids']['trakt'], (int) $argv[2], (int) $argv[3], 'full'); print_r($series); print_r($episode);