mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update Trakt support
This commit is contained in:
@@ -224,7 +224,11 @@ FANARTTV_APIKEY=
|
||||
GOOGLE_BOOKS_API_KEY=
|
||||
ISBNDB_API_KEY=
|
||||
OMDB_APIKEY=
|
||||
# Trakt public metadata API client ID. OAuth tokens are not needed for TV/movie metadata lookups.
|
||||
TRAKTTV_APIKEY=
|
||||
TRAKTTV_TIMEOUT=30
|
||||
TRAKTTV_RETRY_TIMES=3
|
||||
TRAKTTV_RETRY_DELAY=100
|
||||
|
||||
# Temp paths for extraction
|
||||
TEMP_UNRAR_PATH='/var/www/nntmux/resources/tmp/unrar/'
|
||||
|
||||
@@ -189,25 +189,28 @@ class MovieService
|
||||
$data['trailer']
|
||||
);
|
||||
}
|
||||
$imdbId = (str_starts_with($data['ids']['imdb'], 'tt')) ? substr($data['ids']['imdb'], 2) : $data['ids']['imdb'];
|
||||
$imdbId = (string) $data['ids']['imdb'];
|
||||
$imdbId = str_starts_with($imdbId, 'tt') ? substr($imdbId, 2) : $imdbId;
|
||||
$genres = $data['genres'] ?? [];
|
||||
$genre = is_array($genres) ? implode(', ', $genres) : (string) $genres;
|
||||
$cover = 0;
|
||||
if (File::isFile($this->imgSavePath.$imdbId.'-cover.jpg')) {
|
||||
$cover = 1;
|
||||
}
|
||||
|
||||
return $this->update([
|
||||
'genre' => implode(', ', $data['genres']),
|
||||
'genre' => $genre,
|
||||
'imdbid' => $this->checkTraktValue($imdbId),
|
||||
'language' => $this->checkTraktValue($data['language']),
|
||||
'plot' => $this->checkTraktValue($data['overview']),
|
||||
'rating' => $this->checkTraktValue($data['rating']),
|
||||
'tagline' => $this->checkTraktValue($data['tagline']),
|
||||
'title' => $this->checkTraktValue($data['title']),
|
||||
'tmdbid' => $this->checkTraktValue($data['ids']['tmdb']),
|
||||
'traktid' => $this->checkTraktValue($data['ids']['trakt']),
|
||||
'trailer' => $this->checkTraktValue($data['trailer']),
|
||||
'language' => $this->checkTraktValue($data['language'] ?? ''),
|
||||
'plot' => $this->checkTraktValue($data['overview'] ?? ''),
|
||||
'rating' => $this->checkTraktValue($data['rating'] ?? ''),
|
||||
'tagline' => $this->checkTraktValue($data['tagline'] ?? ''),
|
||||
'title' => $this->checkTraktValue($data['title'] ?? ''),
|
||||
'tmdbid' => $this->checkTraktValue($data['ids']['tmdb'] ?? 0),
|
||||
'traktid' => $this->checkTraktValue($data['ids']['trakt'] ?? 0),
|
||||
'trailer' => $this->checkTraktValue($data['trailer'] ?? ''),
|
||||
'cover' => $cover,
|
||||
'year' => $this->checkTraktValue($data['year']),
|
||||
'year' => $this->checkTraktValue($data['year'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
+155
-43
@@ -18,7 +18,11 @@ use Illuminate\Support\Str;
|
||||
* A modern service wrapper for the Trakt.tv API.
|
||||
* Provides methods to fetch movie and TV show information.
|
||||
*
|
||||
* API Documentation: https://trakt.docs.apiary.io/
|
||||
* API Documentation: https://docs.trakt.tv/
|
||||
*
|
||||
* This wrapper intentionally uses public metadata endpoints only. OAuth should
|
||||
* only be added if a future feature needs user-scoped Trakt data such as
|
||||
* watchlists, watched history, private lists, ratings, or sync endpoints.
|
||||
*/
|
||||
class TraktService
|
||||
{
|
||||
@@ -30,6 +34,8 @@ class TraktService
|
||||
|
||||
protected const ID_TYPES = ['imdb', 'tmdb', 'trakt', 'tvdb'];
|
||||
|
||||
protected const SEARCH_TYPES = ['movie', 'show', 'episode', 'person', 'list'];
|
||||
|
||||
/** Minimum seconds between requests to avoid rate limiting (Trakt/Cloudflare). */
|
||||
protected const THROTTLE_SECONDS = 1;
|
||||
|
||||
@@ -46,7 +52,7 @@ class TraktService
|
||||
|
||||
public function __construct(?string $clientId = null)
|
||||
{
|
||||
$this->clientId = $clientId ?? (string) config('nntmux_api.trakttv_api_key', '');
|
||||
$this->clientId = trim($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);
|
||||
@@ -84,8 +90,9 @@ class TraktService
|
||||
protected function getHeaders(): array
|
||||
{
|
||||
return [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
'trakt-api-version' => self::API_VERSION,
|
||||
'trakt-api-version' => (string) self::API_VERSION,
|
||||
'trakt-api-key' => $this->clientId,
|
||||
];
|
||||
}
|
||||
@@ -232,10 +239,7 @@ class TraktService
|
||||
return null;
|
||||
}
|
||||
|
||||
$extended = match ($extended) {
|
||||
'aliases', 'full', 'full,aliases' => $extended,
|
||||
default => 'min',
|
||||
};
|
||||
$extended = $this->normalizeExtended($extended, ['min', 'full', 'aliases', 'full,aliases']);
|
||||
|
||||
// Format the show ID based on type for cache key and API call
|
||||
$formattedId = $this->formatShowIdForApi($showId, $idType);
|
||||
@@ -243,7 +247,7 @@ class TraktService
|
||||
return null;
|
||||
}
|
||||
|
||||
$cacheKey = "trakt_episode_{$idType}_{$showId}_{$season}_{$episode}_{$extended}";
|
||||
$cacheKey = "trakt_episode_{$idType}_{$formattedId}_{$season}_{$episode}_{$extended}";
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($formattedId, $season, $episode, $extended) {
|
||||
return $this->get("shows/{$formattedId}/seasons/{$season}/episodes/{$episode}", [
|
||||
@@ -272,8 +276,9 @@ class TraktService
|
||||
$idPriority = ['trakt', 'tmdb', 'tvdb', 'imdb'];
|
||||
|
||||
foreach ($idPriority as $idType) {
|
||||
if (! empty($ids[$idType]) && $ids[$idType] > 0) {
|
||||
$result = $this->getEpisodeSummary($ids[$idType], $season, $episode, $extended, $idType);
|
||||
$id = $ids[$idType] ?? null;
|
||||
if ($this->isUsableExternalId($id, $idType)) {
|
||||
$result = $this->getEpisodeSummary($id, $season, $episode, $extended, $idType);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
@@ -297,10 +302,16 @@ class TraktService
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($idType) {
|
||||
'imdb' => is_numeric($id) ? 'tt'.str_pad((string) $id, 8, '0', STR_PAD_LEFT) : (string) $id,
|
||||
'trakt', 'tmdb', 'tvdb' => (string) $id,
|
||||
};
|
||||
$formattedId = trim((string) $id);
|
||||
if ($formattedId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($idType === 'imdb') {
|
||||
return $this->formatImdbId($formattedId);
|
||||
}
|
||||
|
||||
return $formattedId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,8 +398,12 @@ class TraktService
|
||||
{
|
||||
if (empty($startDate)) {
|
||||
$startDate = date('Y-m-d');
|
||||
} elseif (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$days = max(1, min($days, 33));
|
||||
|
||||
$cacheKey = "trakt_calendar_{$startDate}_{$days}";
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(6), function () use ($startDate, $days) {
|
||||
@@ -409,12 +424,16 @@ class TraktService
|
||||
return null;
|
||||
}
|
||||
|
||||
$extended = $extended === 'full' ? 'full' : 'min';
|
||||
$slug = Str::slug($movie);
|
||||
$cacheKey = "trakt_movie_{$slug}_{$extended}";
|
||||
$extended = $this->normalizeExtended($extended, ['min', 'full']);
|
||||
$movieId = $this->formatMediaId($movie);
|
||||
if ($movieId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($slug, $extended) {
|
||||
return $this->get("movies/{$slug}", ['extended' => $extended]);
|
||||
$cacheKey = 'trakt_movie_'.md5($movieId).'_'.$extended;
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($movieId, $extended) {
|
||||
return $this->get("movies/{$movieId}", ['extended' => $extended]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -447,24 +466,35 @@ class TraktService
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format IMDB ID with 'tt' prefix if needed
|
||||
if ($idType === 'imdb' && is_numeric($id)) {
|
||||
$id = 'tt'.$id;
|
||||
$formattedId = trim((string) $id);
|
||||
if ($formattedId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($idType === 'imdb') {
|
||||
$formattedId = $this->formatImdbId($formattedId);
|
||||
if ($formattedId === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$params = [
|
||||
'id_type' => $idType,
|
||||
'id' => $id,
|
||||
'id' => $formattedId,
|
||||
];
|
||||
|
||||
if (! empty($mediaType)) {
|
||||
if ($mediaType !== '' && ! in_array($mediaType, self::SEARCH_TYPES, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($mediaType !== '') {
|
||||
$params['type'] = $mediaType;
|
||||
}
|
||||
|
||||
$cacheKey = 'trakt_search_'.$idType.'_'.preg_replace('/[^a-zA-Z0-9_-]/', '_', (string) $id).'_'.($mediaType ?: 'all');
|
||||
$cacheKey = 'trakt_search_'.$idType.'_'.preg_replace('/[^a-zA-Z0-9_-]/', '_', $formattedId).'_'.($params['type'] ?? 'all');
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($id, $idType, $mediaType) {
|
||||
return $this->get('search/'.$idType.'/'.$id, ['type' => $mediaType ?: null]);
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($formattedId, $idType, $params) {
|
||||
return $this->get('search/'.$idType.'/'.$formattedId, array_intersect_key($params, ['type' => true]));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -477,7 +507,8 @@ class TraktService
|
||||
*/
|
||||
public function searchShows(string $query, string $type = 'show'): ?array
|
||||
{
|
||||
if (empty($query)) {
|
||||
$query = trim($query);
|
||||
if ($query === '' || ! in_array($type, self::SEARCH_TYPES, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -504,12 +535,16 @@ class TraktService
|
||||
return null;
|
||||
}
|
||||
|
||||
$extended = $extended === 'full' ? 'full' : 'min';
|
||||
$slug = Str::slug($showKey);
|
||||
$cacheKey = "trakt_show_{$slug}_{$extended}";
|
||||
$extended = $this->normalizeExtended($extended, ['min', 'full']);
|
||||
$showId = $this->formatMediaId($showKey);
|
||||
if ($showId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($slug, $extended) {
|
||||
return $this->get("shows/{$slug}", ['extended' => $extended]);
|
||||
$cacheKey = 'trakt_show_'.md5($showId).'_'.$extended;
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($showId, $extended) {
|
||||
return $this->get("shows/{$showId}", ['extended' => $extended]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -522,15 +557,16 @@ class TraktService
|
||||
*/
|
||||
public function getShowSeasons(string $show, string $extended = 'full'): ?array
|
||||
{
|
||||
if (empty($show)) {
|
||||
$showId = $this->formatMediaId($show);
|
||||
if ($showId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slug = Str::slug($show);
|
||||
$cacheKey = "trakt_seasons_{$slug}_{$extended}";
|
||||
$extended = $this->normalizeExtended($extended, ['min', 'full', 'episodes', 'full,episodes']);
|
||||
$cacheKey = 'trakt_seasons_'.md5($showId).'_'.$extended;
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($slug, $extended) {
|
||||
return $this->get("shows/{$slug}/seasons", ['extended' => $extended]);
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($showId, $extended) {
|
||||
return $this->get("shows/{$showId}/seasons", ['extended' => $extended]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -544,15 +580,16 @@ class TraktService
|
||||
*/
|
||||
public function getSeasonEpisodes(string $show, int $season, string $extended = 'full'): ?array
|
||||
{
|
||||
if (empty($show)) {
|
||||
$showId = $this->formatMediaId($show);
|
||||
if ($showId === null || $season < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slug = Str::slug($show);
|
||||
$cacheKey = "trakt_season_episodes_{$slug}_{$season}_{$extended}";
|
||||
$extended = $this->normalizeExtended($extended, ['min', 'full']);
|
||||
$cacheKey = 'trakt_season_episodes_'.md5($showId)."_{$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]);
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TTL_HOURS), function () use ($showId, $season, $extended) {
|
||||
return $this->get("shows/{$showId}/seasons/{$season}", ['extended' => $extended]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -565,6 +602,8 @@ class TraktService
|
||||
*/
|
||||
public function getTrendingShows(int $limit = 10, string $extended = 'full'): ?array
|
||||
{
|
||||
$limit = $this->normalizeLimit($limit);
|
||||
$extended = $this->normalizeExtended($extended, ['min', 'full']);
|
||||
$cacheKey = "trakt_trending_shows_{$limit}_{$extended}";
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(1), function () use ($limit, $extended) {
|
||||
@@ -584,6 +623,8 @@ class TraktService
|
||||
*/
|
||||
public function getTrendingMovies(int $limit = 10, string $extended = 'full'): ?array
|
||||
{
|
||||
$limit = $this->normalizeLimit($limit);
|
||||
$extended = $this->normalizeExtended($extended, ['min', 'full']);
|
||||
$cacheKey = "trakt_trending_movies_{$limit}_{$extended}";
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(1), function () use ($limit, $extended) {
|
||||
@@ -603,6 +644,8 @@ class TraktService
|
||||
*/
|
||||
public function getPopularShows(int $limit = 10, string $extended = 'full'): ?array
|
||||
{
|
||||
$limit = $this->normalizeLimit($limit);
|
||||
$extended = $this->normalizeExtended($extended, ['min', 'full']);
|
||||
$cacheKey = "trakt_popular_shows_{$limit}_{$extended}";
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(6), function () use ($limit, $extended) {
|
||||
@@ -622,6 +665,8 @@ class TraktService
|
||||
*/
|
||||
public function getPopularMovies(int $limit = 10, string $extended = 'full'): ?array
|
||||
{
|
||||
$limit = $this->normalizeLimit($limit);
|
||||
$extended = $this->normalizeExtended($extended, ['min', 'full']);
|
||||
$cacheKey = "trakt_popular_movies_{$limit}_{$extended}";
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(6), function () use ($limit, $extended) {
|
||||
@@ -642,6 +687,16 @@ class TraktService
|
||||
Cache::forget("trakt_show_{$slug}_full");
|
||||
Cache::forget("trakt_seasons_{$slug}_full");
|
||||
Cache::forget("trakt_seasons_{$slug}_min");
|
||||
|
||||
$showId = $this->formatMediaId($show);
|
||||
if ($showId !== null) {
|
||||
Cache::forget('trakt_show_'.md5($showId).'_min');
|
||||
Cache::forget('trakt_show_'.md5($showId).'_full');
|
||||
Cache::forget('trakt_seasons_'.md5($showId).'_min');
|
||||
Cache::forget('trakt_seasons_'.md5($showId).'_full');
|
||||
Cache::forget('trakt_seasons_'.md5($showId).'_episodes');
|
||||
Cache::forget('trakt_seasons_'.md5($showId).'_full,episodes');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -652,5 +707,62 @@ class TraktService
|
||||
$slug = Str::slug($movie);
|
||||
Cache::forget("trakt_movie_{$slug}_min");
|
||||
Cache::forget("trakt_movie_{$slug}_full");
|
||||
|
||||
$movieId = $this->formatMediaId($movie);
|
||||
if ($movieId !== null) {
|
||||
Cache::forget('trakt_movie_'.md5($movieId).'_min');
|
||||
Cache::forget('trakt_movie_'.md5($movieId).'_full');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize IMDb IDs for Trakt endpoints without padding away meaningful
|
||||
* leading zeroes (for example, tt0137523 must stay tt0137523).
|
||||
*/
|
||||
protected function formatImdbId(string $id): ?string
|
||||
{
|
||||
if (preg_match('/^tt\d{6,}$/i', $id) === 1) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$digits = preg_replace('/\D/', '', $id);
|
||||
|
||||
return $digits !== null && preg_match('/^\d{6,}$/', $digits) === 1 ? 'tt'.$digits : null;
|
||||
}
|
||||
|
||||
protected function formatMediaId(int|string $id): ?string
|
||||
{
|
||||
$id = trim((string) $id);
|
||||
if ($id === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return str_starts_with(strtolower($id), 'tt') ? $this->formatImdbId($id) : Str::slug($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $allowed
|
||||
*/
|
||||
protected function normalizeExtended(string $extended, array $allowed): string
|
||||
{
|
||||
return in_array($extended, $allowed, true) ? $extended : $allowed[0];
|
||||
}
|
||||
|
||||
protected function normalizeLimit(int $limit): int
|
||||
{
|
||||
return max(1, min($limit, 100));
|
||||
}
|
||||
|
||||
protected function isUsableExternalId(mixed $id, string $idType): bool
|
||||
{
|
||||
if ($id === null || $id === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($idType === 'imdb') {
|
||||
return $this->formatImdbId((string) $id) !== null;
|
||||
}
|
||||
|
||||
return is_numeric($id) ? (int) $id > 0 : trim((string) $id) !== '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ use App\Services\TvProcessing\TvProcessingResult;
|
||||
*/
|
||||
class TraktPipe extends AbstractTvProviderPipe
|
||||
{
|
||||
// Video type and source constants (matching Videos class protected constants)
|
||||
// Video type and source constants (matching AbstractTvProvider constants)
|
||||
private const TYPE_TV = 0;
|
||||
|
||||
private const SOURCE_TRAKT = 5;
|
||||
private const SOURCE_TRAKT = 4;
|
||||
|
||||
protected int $priority = 50;
|
||||
|
||||
|
||||
@@ -520,7 +520,7 @@ abstract class AbstractTvProvider extends BaseVideoProvider
|
||||
$following = '[^a-z0-9]([(|\[]\w+[)|\]]\s)*?(\d\d-\d\d|\d{1,3}x\d{2,3}|\(?(19|20)\d{2}\)?|(480|720|1080|2160)[ip]|AAC2?|BD-?Rip|Blu-?Ray|D0?\d|DD5|DiVX|DLMux|DTS|DVD(-?Rip)?|E\d{2,3}|[HX][\-_. ]?26[45]|ITA(-ENG)?|HEVC|[HPS]DTV|PROPER|REPACK|Season|Episode|S\d+[^a-z0-9]?((E\d+)[abr]?)*|WEB[\-_. ]?(DL|Rip)|XViD)[^a-z0-9]?';
|
||||
|
||||
// Handle fansub/release group prefixes like [SubsPlease], [Erai-raws], [ASW], etc.
|
||||
$cleanRelname = preg_replace('/^\[[^\]]+\][_\s]*/i', '', $relname);
|
||||
$cleanRelname = preg_replace('/^\[[^]]+][_\s]*/i', '', $relname);
|
||||
|
||||
if (preg_match('/^([^a-z0-9]{2,}|(sample|proof|repost)-)(?P<name>[\w .\-!&]+?)'.$following.'/i', $cleanRelname, $hits)) {
|
||||
$showName = $hits['name'];
|
||||
@@ -888,10 +888,10 @@ abstract class AbstractTvProvider extends BaseVideoProvider
|
||||
$required = ['name', 'season_number', 'episode_number', 'air_date', 'overview'];
|
||||
break;
|
||||
case 'traktS':
|
||||
$required = ['title', 'ids', 'overview', 'first_aired', 'airs', 'country'];
|
||||
$required = ['title', 'ids'];
|
||||
break;
|
||||
case 'traktE':
|
||||
$required = ['title', 'season', 'number', 'overview', 'first_aired'];
|
||||
$required = ['title', 'season', 'number'];
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ class TraktProvider extends AbstractTvProvider
|
||||
/**
|
||||
* The URL to grab the TV fanart.
|
||||
*/
|
||||
public string $fanartUrl;
|
||||
public string $fanartUrl = '';
|
||||
|
||||
/**
|
||||
* The localized (network airing) timezone of the show.
|
||||
*/
|
||||
private string $localizedTZ;
|
||||
private string $localizedTZ = '';
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
@@ -278,6 +278,12 @@ class TraktProvider extends AbstractTvProvider
|
||||
{
|
||||
$return = false;
|
||||
|
||||
if ($videoId > 0 && (int) $series === -1 && (int) $episode === -1) {
|
||||
$this->addEpisodesForShow($siteId, $videoId);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have a video ID, get all available external IDs for fallback
|
||||
if ($videoId > 0) {
|
||||
$ids = $this->getAllSiteIdsFromVideoID($videoId);
|
||||
@@ -352,15 +358,20 @@ class TraktProvider extends AbstractTvProvider
|
||||
sleep(1);
|
||||
|
||||
foreach ($response as $show) {
|
||||
if (! is_bool($show)) {
|
||||
if (is_array($show) && isset($show['show']) && is_array($show['show'])) {
|
||||
$showTitle = (string) ($show['show']['title'] ?? '');
|
||||
if ($showTitle === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for exact title match first and then terminate if found
|
||||
if ($show['show']['title'] === $name) {
|
||||
if (strcasecmp($showTitle, $name) === 0) {
|
||||
$highest = $show;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check each show title for similarity and then find the highest similar value
|
||||
$matchPercent = $this->checkMatch($show['show']['title'], $name, self::MATCH_PROBABILITY);
|
||||
$matchPercent = $this->checkMatch($showTitle, $name, self::MATCH_PROBABILITY);
|
||||
|
||||
// If new match has a higher percentage, set as new matched title
|
||||
if ($matchPercent > $highestMatch) {
|
||||
@@ -369,7 +380,7 @@ class TraktProvider extends AbstractTvProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($highest !== null) {
|
||||
if ($highest !== null && ! empty($highest['show']['ids']['trakt'])) {
|
||||
$fullShow = $this->client->getShowSummary($highest['show']['ids']['trakt']);
|
||||
if ($this->checkRequiredAttr($fullShow, 'traktS')) {
|
||||
$return = $this->formatShowInfo($fullShow);
|
||||
@@ -387,31 +398,39 @@ class TraktProvider extends AbstractTvProvider
|
||||
*/
|
||||
public function formatShowInfo(mixed $show): array
|
||||
{
|
||||
preg_match('/tt(?P<imdbid>\d{6,})$/i', (string) ($show['ids']['imdb'] ?? ''), $imdb);
|
||||
$this->posterUrl = $show['images']['poster']['thumb'] ?? '';
|
||||
$this->fanartUrl = $show['images']['fanart']['thumb'] ?? '';
|
||||
$this->localizedTZ = $show['airs']['timezone'] ?? '';
|
||||
if (! is_array($show)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ids = is_array($show['ids'] ?? null) ? $show['ids'] : [];
|
||||
$airs = is_array($show['airs'] ?? null) ? $show['airs'] : [];
|
||||
$images = is_array($show['images'] ?? null) ? $show['images'] : [];
|
||||
|
||||
preg_match('/tt(?P<imdbid>\d{6,})$/i', (string) ($ids['imdb'] ?? ''), $imdb);
|
||||
$this->posterUrl = is_array($images['poster'] ?? null) ? (string) ($images['poster']['thumb'] ?? '') : '';
|
||||
$this->fanartUrl = is_array($images['fanart'] ?? null) ? (string) ($images['fanart']['thumb'] ?? '') : '';
|
||||
$this->localizedTZ = (string) ($airs['timezone'] ?? '');
|
||||
|
||||
$imdbId = (string) ($imdb['imdbid'] ?? '');
|
||||
$tvdbId = $show['ids']['tvdb'] ?? 0;
|
||||
$tvdbId = (int) ($ids['tvdb'] ?? 0);
|
||||
|
||||
// Look up TVMaze ID using TVDB or IMDB
|
||||
$tvmazeId = $this->lookupTvMazeId($tvdbId, $imdbId);
|
||||
|
||||
return [
|
||||
'type' => parent::TYPE_TV,
|
||||
'title' => $show['title'],
|
||||
'summary' => $show['overview'],
|
||||
'started' => Carbon::parse($show['first_aired'], $this->localizedTZ)->format('Y-m-d'),
|
||||
'publisher' => $show['network'],
|
||||
'country' => $show['country'],
|
||||
'title' => (string) ($show['title'] ?? ''),
|
||||
'summary' => (string) ($show['overview'] ?? ''),
|
||||
'started' => $this->formatTraktDate($show['first_aired'] ?? null),
|
||||
'publisher' => (string) ($show['network'] ?? ''),
|
||||
'country' => (string) ($show['country'] ?? ''),
|
||||
'source' => parent::SOURCE_TRAKT,
|
||||
'imdb' => $imdbId,
|
||||
'tvdb' => $tvdbId,
|
||||
'trakt' => $show['ids']['trakt'],
|
||||
'tvrage' => $show['ids']['tvrage'] ?? 0,
|
||||
'trakt' => (int) ($ids['trakt'] ?? 0),
|
||||
'tvrage' => (int) ($ids['tvrage'] ?? 0),
|
||||
'tvmaze' => $tvmazeId,
|
||||
'tmdb' => $show['ids']['tmdb'] ?? 0,
|
||||
'tmdb' => (int) ($ids['tmdb'] ?? 0),
|
||||
'aliases' => isset($show['aliases']) && ! empty($show['aliases']) ? $show['aliases'] : '',
|
||||
'localzone' => $this->localizedTZ,
|
||||
];
|
||||
@@ -460,14 +479,63 @@ class TraktProvider extends AbstractTvProvider
|
||||
*/
|
||||
public function formatEpisodeInfo(mixed $episode): array
|
||||
{
|
||||
$season = (int) ($episode['season'] ?? 0);
|
||||
$episodeNumber = (int) ($episode['number'] ?? ($episode['episode'] ?? 0));
|
||||
|
||||
return [
|
||||
'title' => (string) $episode['title'],
|
||||
'series' => (int) $episode['season'],
|
||||
'title' => (string) ($episode['title'] ?? ''),
|
||||
'series' => $season,
|
||||
// Prefer the Trakt 'number' field for episode number, fall back to 'episode' if present
|
||||
'episode' => (int) ($episode['number'] ?? ($episode['episode'] ?? 0)),
|
||||
'se_complete' => 'S'.sprintf('%02d', $episode['season']).'E'.sprintf('%02d', ($episode['number'] ?? ($episode['episode'] ?? 0))),
|
||||
'firstaired' => Carbon::parse($episode['first_aired'], $this->localizedTZ)->format('Y-m-d'),
|
||||
'summary' => (string) $episode['overview'],
|
||||
'episode' => $episodeNumber,
|
||||
'se_complete' => 'S'.sprintf('%02d', $season).'E'.sprintf('%02d', $episodeNumber),
|
||||
'firstaired' => $this->formatTraktDate($episode['first_aired'] ?? null),
|
||||
'summary' => (string) ($episode['overview'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private function addEpisodesForShow(int|string $siteId, int $videoId): void
|
||||
{
|
||||
$seasons = $this->client->getShowSeasons((string) $siteId, 'episodes');
|
||||
if (! is_array($seasons)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($seasons as $seasonInfo) {
|
||||
if (! is_array($seasonInfo)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seasonNumber = (int) ($seasonInfo['number'] ?? -1);
|
||||
if ($seasonNumber < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$episodes = is_array($seasonInfo['episodes'] ?? null)
|
||||
? $seasonInfo['episodes']
|
||||
: $this->client->getSeasonEpisodes((string) $siteId, $seasonNumber, 'full');
|
||||
|
||||
if (! is_array($episodes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($episodes as $singleEpisode) {
|
||||
if (is_array($singleEpisode) && $this->checkRequiredAttr($singleEpisode, 'traktE')) {
|
||||
$this->addEpisode($videoId, $this->formatEpisodeInfo($singleEpisode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function formatTraktDate(mixed $date): string
|
||||
{
|
||||
if (empty($date)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse((string) $date, $this->localizedTZ !== '' ? $this->localizedTZ : null)->format('Y-m-d');
|
||||
} catch (\Throwable) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,7 @@ return [
|
||||
'imdbapi_dev_cooldown_seconds' => env('IMDBAPI_DEV_COOLDOWN_SECONDS', 300),
|
||||
'omdb_api_key' => env('OMDB_APIKEY', ''),
|
||||
'trakttv_api_key' => env('TRAKTTV_APIKEY', ''),
|
||||
'trakttv_timeout' => env('TRAKTTV_TIMEOUT', 30),
|
||||
'trakttv_retry_times' => env('TRAKTTV_RETRY_TIMES', 3),
|
||||
'trakttv_retry_delay' => env('TRAKTTV_RETRY_DELAY', 100),
|
||||
];
|
||||
|
||||
@@ -50,4 +50,130 @@ class TraktServiceTest extends TestCase
|
||||
&& $request['extended'] === 'full';
|
||||
});
|
||||
}
|
||||
|
||||
public function test_public_metadata_requests_use_api_key_headers_without_oauth(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.trakt.tv/movies/tt0137523*' => Http::response([
|
||||
'title' => 'Fight Club',
|
||||
'ids' => [
|
||||
'imdb' => 'tt0137523',
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$summary = $this->service->getMovieSummary('tt0137523', 'full');
|
||||
|
||||
$this->assertNotNull($summary);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
$headers = array_change_key_case($request->headers(), CASE_LOWER);
|
||||
|
||||
return ($headers['trakt-api-key'][0] ?? null) === 'test_trakt_key'
|
||||
&& ($headers['trakt-api-version'][0] ?? null) === '2'
|
||||
&& ! array_key_exists('authorization', $headers);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_search_by_id_formats_imdb_id_without_padding(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.trakt.tv/search/imdb/tt0137523*' => Http::response([
|
||||
[
|
||||
'type' => 'movie',
|
||||
'movie' => [
|
||||
'title' => 'Fight Club',
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$results = $this->service->searchById('0137523', 'imdb', 'movie');
|
||||
|
||||
$this->assertIsArray($results);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), 'https://api.trakt.tv/search/imdb/tt0137523')
|
||||
&& ! str_contains($request->url(), 'tt00137523')
|
||||
&& $request['type'] === 'movie';
|
||||
});
|
||||
}
|
||||
|
||||
public function test_episode_summary_with_imdb_id_preserves_leading_zeroes(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.trakt.tv/shows/tt0137523/seasons/1/episodes/1*' => Http::response([
|
||||
'title' => 'Pilot',
|
||||
'season' => 1,
|
||||
'number' => 1,
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$episode = $this->service->getEpisodeSummary('0137523', 1, 1, 'full', 'imdb');
|
||||
|
||||
$this->assertIsArray($episode);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), 'https://api.trakt.tv/shows/tt0137523/seasons/1/episodes/1')
|
||||
&& ! str_contains($request->url(), 'tt00137523')
|
||||
&& $request['extended'] === 'full';
|
||||
});
|
||||
}
|
||||
|
||||
public function test_episode_summary_fallback_accepts_tt_prefixed_imdb_id(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.trakt.tv/shows/tt0137523/seasons/1/episodes/1*' => Http::response([
|
||||
'title' => 'Pilot',
|
||||
'season' => 1,
|
||||
'number' => 1,
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$episode = $this->service->getEpisodeSummaryWithFallback(['imdb' => 'tt0137523'], 1, 1, 'full');
|
||||
|
||||
$this->assertIsArray($episode);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), 'https://api.trakt.tv/shows/tt0137523/seasons/1/episodes/1')
|
||||
&& $request['extended'] === 'full';
|
||||
});
|
||||
}
|
||||
|
||||
public function test_search_by_id_omits_empty_type_query_parameter(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.trakt.tv/search/trakt/1390*' => Http::response([], 200),
|
||||
]);
|
||||
|
||||
$this->service->searchById(1390, 'trakt');
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
parse_str(parse_url($request->url(), PHP_URL_QUERY) ?: '', $query);
|
||||
|
||||
return str_contains($request->url(), 'https://api.trakt.tv/search/trakt/1390')
|
||||
&& ! array_key_exists('type', $query);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_get_show_seasons_supports_episode_extended_public_endpoint(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.trakt.tv/shows/1390/seasons*' => Http::response([
|
||||
[
|
||||
'number' => 1,
|
||||
'episodes' => [],
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$seasons = $this->service->getShowSeasons('1390', 'episodes');
|
||||
|
||||
$this->assertIsArray($seasons);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), 'https://api.trakt.tv/shows/1390/seasons')
|
||||
&& $request['extended'] === 'episodes';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user