From 26e22e9dba6a0804643d4e9f752e766d990eb3ed Mon Sep 17 00:00:00 2001 From: TOomaAh Date: Fri, 26 Jul 2024 23:56:22 +0200 Subject: [PATCH] feat(tvdb): get tv seasons/episodes with tvdb --- jellyseerr-api.yml | 89 +++++- server/api/externalapi.ts | 2 +- server/api/indexer/index.ts | 25 ++ .../api/{ => indexer}/themoviedb/constants.ts | 0 server/api/{ => indexer}/themoviedb/index.ts | 5 +- .../{ => indexer}/themoviedb/interfaces.ts | 0 server/api/indexer/tvdb/index.ts | 226 +++++++++++++++ server/api/indexer/tvdb/interfaces.ts | 143 +++++++++ server/entity/MediaRequest.ts | 6 +- server/entity/UserSettings.ts | 3 + server/entity/Watchlist.ts | 2 +- server/lib/cache.ts | 7 +- server/lib/scanners/baseScanner.ts | 2 +- server/lib/scanners/jellyfin/index.ts | 4 +- server/lib/scanners/plex/index.ts | 2 +- server/lib/scanners/sonarr/index.ts | 2 +- server/lib/search.ts | 4 +- server/lib/settings/index.ts | 16 + server/models/Collection.ts | 2 +- server/models/Movie.ts | 2 +- server/models/Person.ts | 2 +- server/models/Search.ts | 2 +- server/models/Tv.ts | 2 +- server/models/common.ts | 2 +- server/routes/collection.ts | 2 +- server/routes/discover.ts | 6 +- server/routes/index.ts | 6 +- server/routes/media.ts | 2 +- server/routes/movie.ts | 2 +- server/routes/person.ts | 2 +- server/routes/search.ts | 4 +- server/routes/service.ts | 2 +- server/routes/settings/index.ts | 2 + server/routes/settings/tvdb.ts | 46 +++ server/routes/tv.ts | 30 +- server/subscriber/IssueCommentSubscriber.ts | 2 +- server/subscriber/IssueSubscriber.ts | 2 +- server/utils/typeHelpers.ts | 2 +- .../Discover/CreateSlider/index.tsx | 2 +- .../Discover/DiscoverMovieKeyword/index.tsx | 2 +- .../Discover/DiscoverMovies/index.tsx | 2 +- src/components/Discover/DiscoverTv/index.tsx | 2 +- .../Discover/DiscoverTvKeyword/index.tsx | 2 +- src/components/GenreTag/index.tsx | 2 +- .../RequestModal/TvRequestModal.tsx | 2 +- src/components/Selector/index.tsx | 2 +- src/components/Settings/SettingsLayout.tsx | 5 + src/components/Settings/SettingsTvdb.tsx | 273 ++++++++++++++++++ src/components/TvDetails/Season/index.tsx | 5 +- src/components/TvDetails/index.tsx | 3 +- src/pages/settings/tvdb.tsx | 16 + 51 files changed, 922 insertions(+), 56 deletions(-) create mode 100644 server/api/indexer/index.ts rename server/api/{ => indexer}/themoviedb/constants.ts (100%) rename server/api/{ => indexer}/themoviedb/index.ts (99%) rename server/api/{ => indexer}/themoviedb/interfaces.ts (100%) create mode 100644 server/api/indexer/tvdb/index.ts create mode 100644 server/api/indexer/tvdb/interfaces.ts create mode 100644 server/routes/settings/tvdb.ts create mode 100644 src/components/Settings/SettingsTvdb.tsx create mode 100644 src/pages/settings/tvdb.tsx diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 4254ba43..20e170d1 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -519,6 +519,18 @@ components: serverID: type: string readOnly: true + TvdbSettings: + type: object + properties: + apiKey: + type: string + example: 'apikey' + pin: + type: string + example: 'ABCDEFGH' + use: + type: boolean + example: true TautulliSettings: type: object properties: @@ -2568,6 +2580,75 @@ paths: type: string thumb: type: string + /settings/tvdb: + get: + summary: Get TVDB settings + description: Retrieves current TVDB settings. + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TvdbSettings' + put: + summary: Update TVDB settings + description: Updates TVDB settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TvdbSettings' + responses: + '200': + description: 'Values were successfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/TvdbSettings' + /settings/tvdb/test: + post: + summary: Test TVDB configuration + description: Tests if the TVDB configuration is valid. Returns a list of available languages on success. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + apiKey: + type: string + example: yourapikey + pin: + type: string + example: yourpin + required: + - apiKey + responses: + '200': + description: Succesfully connected to TVDB + content: + application/json: + schema: + type: object + properties: + languages: + type: array + items: + type: object + properties: + id: + type: number + name: + type: string /settings/tautulli: get: summary: Get Tautulli settings @@ -6472,7 +6553,7 @@ paths: application/json: schema: $ref: '#/components/schemas/TvDetails' - /tv/{tvId}/season/{seasonId}: + /tv/{tvId}/season/{seasonNumber}/{seasonId}: get: summary: Get season details and episode list description: Returns season details with a list of episodes in a JSON object. @@ -6491,6 +6572,12 @@ paths: schema: type: number example: 1 + - in: path + name: seasonNumber + required: true + schema: + type: number + example: 123456 - in: query name: language schema: diff --git a/server/api/externalapi.ts b/server/api/externalapi.ts index 0d2016f7..f25fc547 100644 --- a/server/api/externalapi.ts +++ b/server/api/externalapi.ts @@ -10,7 +10,7 @@ const DEFAULT_TTL = 300; // 10 seconds default rolling buffer (in ms) const DEFAULT_ROLLING_BUFFER = 10000; -interface ExternalAPIOptions { +export interface ExternalAPIOptions { nodeCache?: NodeCache; headers?: Record; rateLimit?: { diff --git a/server/api/indexer/index.ts b/server/api/indexer/index.ts new file mode 100644 index 00000000..aa59540b --- /dev/null +++ b/server/api/indexer/index.ts @@ -0,0 +1,25 @@ +import type { + TmdbSeasonWithEpisodes, + TmdbTvDetails, +} from '@server/api/indexer/themoviedb/interfaces'; + +export interface TvShowIndexer { + getTvShow({ + tvId, + language, + }: { + tvId: number; + language?: string; + }): Promise; + getTvSeason({ + tvId, + seasonId, + seasonNumber, + language, + }: { + tvId: number; + seasonId: number; + seasonNumber: number; + language?: string; + }): Promise; +} diff --git a/server/api/themoviedb/constants.ts b/server/api/indexer/themoviedb/constants.ts similarity index 100% rename from server/api/themoviedb/constants.ts rename to server/api/indexer/themoviedb/constants.ts diff --git a/server/api/themoviedb/index.ts b/server/api/indexer/themoviedb/index.ts similarity index 99% rename from server/api/themoviedb/index.ts rename to server/api/indexer/themoviedb/index.ts index 9d6f5089..543ee69c 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/indexer/themoviedb/index.ts @@ -1,4 +1,5 @@ import ExternalAPI from '@server/api/externalapi'; +import type { TvShowIndexer } from '@server/api/indexer'; import cacheManager from '@server/lib/cache'; import { getSettings } from '@server/lib/settings'; import { sortBy } from 'lodash'; @@ -120,14 +121,14 @@ interface DiscoverTvOptions { certificationCountry?: string; } -class TheMovieDb extends ExternalAPI { +class TheMovieDb extends ExternalAPI implements TvShowIndexer { private locale: string; private discoverRegion?: string; private originalLanguage?: string; constructor({ discoverRegion, originalLanguage, - }: { discoverRegion?: string; originalLanguage?: string } = {}) { + }: { discoverRegion?: string; originalLanguage?: string } = {}) { super( 'https://api.themoviedb.org/3', { diff --git a/server/api/themoviedb/interfaces.ts b/server/api/indexer/themoviedb/interfaces.ts similarity index 100% rename from server/api/themoviedb/interfaces.ts rename to server/api/indexer/themoviedb/interfaces.ts diff --git a/server/api/indexer/tvdb/index.ts b/server/api/indexer/tvdb/index.ts new file mode 100644 index 00000000..5419420a --- /dev/null +++ b/server/api/indexer/tvdb/index.ts @@ -0,0 +1,226 @@ +import ExternalAPI from '@server/api/externalapi'; +import type { TvShowIndexer } from '@server/api/indexer'; +import TheMovieDb from '@server/api/indexer/themoviedb'; +import type { + TmdbSeasonWithEpisodes, + TmdbTvDetails, +} from '@server/api/indexer/themoviedb/interfaces'; +import type { + TvdbEpisodeTranslation, + TvdbLoginResponse, + TvdbSeasonDetails, + TvdbTvDetails, +} from '@server/api/indexer/tvdb/interfaces'; +import cacheManager from '@server/lib/cache'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; + +class Tvdb extends ExternalAPI implements TvShowIndexer { + static instance: Tvdb; + private dateTokenExpires?: Date; + private pin?: string; + + private constructor(apiKey: string, pin?: string) { + super( + 'https://api4.thetvdb.com/v4', + { + apiKey: apiKey, + }, + { + nodeCache: cacheManager.getCache('tvdb').data, + rateLimit: { + maxRPS: 50, + id: 'tmdb', + }, + } + ); + this.pin = pin; + } + + public static async getInstance() { + if (!this.instance) { + const settings = getSettings(); + if (!settings.tvdb.apiKey) { + throw new Error('TVDB API key is not set'); + } + this.instance = new Tvdb(settings.tvdb.apiKey, settings.tvdb.pin); + await this.instance.login(); + logger.info( + 'Tvdb instance created with token => ' + + this.instance.defaultHeaders.Authorization + ); + } + return this.instance; + } + + async login() { + try { + const res = await this.post('/login', { + apiKey: this.params.apiKey, + pin: this.pin, + }); + this.defaultHeaders.Authorization = `Bearer ${res.data.token}`; + this.dateTokenExpires = new Date(); + this.dateTokenExpires.setMonth(this.dateTokenExpires.getMonth() + 1); + return res; + } catch (error) { + throw new Error(`[TVDB] Login failed: ${error.message}`); + } + } + + public getTvShow = async ({ + tvId, + language = 'en', + }: { + tvId: number; + language?: string; + }): Promise => { + try { + const tmdb = new TheMovieDb(); + const tmdbTvShow = await tmdb.getTvShow({ tvId: tvId }); + + const tvdbId = tmdbTvShow.external_ids.tvdb_id; + + if (!tvdbId) { + return tmdbTvShow; + } + + const data = await this.get( + `/series/${tvdbId}/extended`, + { + short: 'true', + }, + 43200 + ); + + const correctSeasons = data.data.seasons.filter( + (season: TvdbSeasonDetails) => + season.id && season.number > 0 && season.type.name === 'Aired Order' + ); + + tmdbTvShow.seasons = []; + + for (const season of correctSeasons) { + if (season.id) { + logger.info(`Fetching TV season ${season.id}`); + + try { + const tvdbSeason = await this.getTvSeason({ + tvId: tvdbId, + seasonNumber: season.id, + language, + }); + const seasonData = { + id: season.id, + episode_count: tvdbSeason.episodes.length, + name: tvdbSeason.name, + overview: tvdbSeason.overview, + season_number: season.number, + poster_path: '', + air_date: '', + image: tvdbSeason.poster_path, + }; + + tmdbTvShow.seasons.push(seasonData); + } catch (error) { + logger.error( + `Failed to get season ${season.id} for TV show ${tvdbId}: ${error.message}`, + { + label: 'Tvdb', + message: `Failed to get season ${season.id} for TV show ${tvdbId}`, + } + ); + } + } + } + + return tmdbTvShow; + } catch (error) { + throw new Error( + `[TVDB] Failed to fetch TV show details: ${error.message}` + ); + } + }; + + getEpisode = async ( + episodeId: number, + language: string + ): Promise => { + try { + const tvdbEpisode = await this.get( + `/episodes/${episodeId}/translations/${language}`, + {}, + 43200 + ); + + return tvdbEpisode; + } catch (error) { + throw new Error( + `[TVDB] Failed to fetch TV episode details: ${error.message}` + ); + } + }; + + public getTvSeason = async ({ + tvId, + seasonNumber, + language = 'en', + }: { + tvId: number; + seasonNumber: number; + language?: string; + }): Promise => { + if (seasonNumber === 0) { + return { + episodes: [], + external_ids: { + tvdb_id: tvId, + }, + name: '', + overview: '', + id: seasonNumber, + air_date: '', + season_number: 0, + }; + } + try { + const tvdbSeason = await this.get( + `/seasons/${seasonNumber}/extended`, + { lang: language }, + 43200 + ); + + const episodes = tvdbSeason.data.episodes.map((episode) => ({ + id: episode.id, + air_date: episode.aired, + episode_number: episode.number, + name: episode.name, + overview: episode.overview || '', + season_number: episode.seasonNumber, + production_code: '', + show_id: tvId, + still_path: episode.image, + vote_average: 1, + vote_cuont: 1, + })); + + return { + episodes: episodes, + external_ids: { + tvdb_id: tvdbSeason.seriesId, + }, + name: '', + overview: '', + id: tvdbSeason.id, + air_date: tvdbSeason.year, + season_number: tvdbSeason.number, + }; + } catch (error) { + throw new Error( + `[TVDB] Failed to fetch TV season details: ${error.message}` + ); + } + }; +} + +export default Tvdb; diff --git a/server/api/indexer/tvdb/interfaces.ts b/server/api/indexer/tvdb/interfaces.ts new file mode 100644 index 00000000..c45e47a9 --- /dev/null +++ b/server/api/indexer/tvdb/interfaces.ts @@ -0,0 +1,143 @@ +export interface TvdbBaseResponse { + data: T; + errors: any; +} + +export interface TvdbLoginResponse extends TvdbBaseResponse<{ token: string }> { + data: { token: string }; +} + +interface TvDetailsAliases { + language: string; + name: string; +} + +interface TvDetailsStatus { + id: number; + name: string; + recordType: string; + keepUpdated: boolean; +} + +export interface TvdbTvDetails extends TvdbBaseResponse { + id: number; + name: string; + slug: string; + image: string; + nameTranslations: string[]; + overwiewTranslations: string[]; + aliases: TvDetailsAliases[]; + firstAired: Date; + lastAired: Date; + nextAired: Date | string; + score: number; + status: TvDetailsStatus; + originalCountry: string; + originalLanguage: string; + defaultSeasonType: string; + isOrderRandomized: boolean; + lastUpdated: Date; + averageRuntime: number; + seasons: TvdbSeasonDetails[]; +} + +interface TvdbCompanyType { + companyTypeId: number; + companyTypeName: string; +} + +interface TvdbParentCompany { + id?: number; + name?: string; + relation?: { + id?: number; + typeName?: string; + }; +} + +interface TvdbCompany { + id: number; + name: string; + slug: string; + nameTranslations?: string[]; + overviewTranslations?: string[]; + aliases?: string[]; + country: string; + primaryCompanyType: number; + activeDate: string; + inactiveDate?: string; + companyType: TvdbCompanyType; + parentCompany: TvdbParentCompany; + tagOptions?: string[]; +} + +interface TvdbType { + id: number; + name: string; + type: string; + alternateName?: string; +} + +interface TvdbArtwork { + id: number; + image: string; + thumbnail: string; + language: string; + type: number; + score: number; + width: number; + height: number; + includesText: boolean; +} + +interface TvdbEpisode { + id: number; + seriesId: number; + name: string; + aired: string; + runtime: number; + nameTranslations: string[]; + overview?: string; + overviewTranslations: string[]; + image: string; + imageType: number; + isMovie: number; + seasons?: string[]; + number: number; + absoluteNumber: number; + seasonNumber: number; + lastUpdated: string; + finaleType?: string; + year: string; +} + +export interface TvdbSeasonDetails extends TvdbBaseResponse { + id: number; + seriesId: number; + type: TvdbType; + number: number; + nameTranslations: string[]; + overviewTranslations: string[]; + image: string; + imageType: number; + companies: { + studio: TvdbCompany[]; + network: TvdbCompany[]; + production: TvdbCompany[]; + distributor: TvdbCompany[]; + special_effects: TvdbCompany[]; + }; + lastUpdated: string; + year: string; + episodes: TvdbEpisode[]; + trailers: string[]; + artwork: TvdbArtwork[]; + tagOptions?: string[]; +} + +export interface TvdbEpisodeTranslation + extends TvdbBaseResponse { + name: string; + overview: string; + language: string; +} diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index cdfa17c3..b1a017a9 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -1,6 +1,6 @@ -import TheMovieDb from '@server/api/themoviedb'; -import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; -import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; +import TheMovieDb from '@server/api/indexer/themoviedb'; +import { ANIME_KEYWORD_ID } from '@server/api/indexer/themoviedb/constants'; +import type { TmdbKeyword } from '@server/api/indexer/themoviedb/interfaces'; import { MediaRequestStatus, MediaStatus, diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 82671fe3..0fcebaa1 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -39,6 +39,9 @@ export class UserSettings { @Column({ nullable: true }) public originalLanguage?: string; + @Column({ nullable: true }) + public tvdbToken?: string; + @Column({ nullable: true }) public pgpKey?: string; diff --git a/server/entity/Watchlist.ts b/server/entity/Watchlist.ts index 10c26246..b39b9ea7 100644 --- a/server/entity/Watchlist.ts +++ b/server/entity/Watchlist.ts @@ -1,4 +1,4 @@ -import TheMovieDb from '@server/api/themoviedb'; +import TheMovieDb from '@server/api/indexer/themoviedb'; import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; diff --git a/server/lib/cache.ts b/server/lib/cache.ts index 51d0e08f..64b5c79e 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -9,7 +9,8 @@ export type AvailableCacheIds = | 'github' | 'plexguid' | 'plextv' - | 'plexwatchlist'; + | 'plexwatchlist' + | 'tvdb'; const DEFAULT_TTL = 300; const DEFAULT_CHECK_PERIOD = 120; @@ -70,6 +71,10 @@ class CacheManager { checkPeriod: 60, }), plexwatchlist: new Cache('plexwatchlist', 'Plex Watchlist'), + tvdb: new Cache('tvdb', 'The TVDB API', { + stdTtl: 21600, + checkPeriod: 60 * 30, + }), }; public getCache(id: AvailableCacheIds): Cache { diff --git a/server/lib/scanners/baseScanner.ts b/server/lib/scanners/baseScanner.ts index b78ea811..606505cb 100644 --- a/server/lib/scanners/baseScanner.ts +++ b/server/lib/scanners/baseScanner.ts @@ -1,4 +1,4 @@ -import TheMovieDb from '@server/api/themoviedb'; +import TheMovieDb from '@server/api/indexer/themoviedb'; import { MediaStatus, MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index bfef4f7e..fc27cd85 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -1,7 +1,7 @@ +import TheMovieDb from '@server/api/indexer/themoviedb'; +import type { TmdbTvDetails } from '@server/api/indexer/themoviedb/interfaces'; import type { JellyfinLibraryItem } from '@server/api/jellyfin'; import JellyfinAPI from '@server/api/jellyfin'; -import TheMovieDb from '@server/api/themoviedb'; -import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces'; import { MediaStatus, MediaType } from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts index 9dee904a..1964f654 100644 --- a/server/lib/scanners/plex/index.ts +++ b/server/lib/scanners/plex/index.ts @@ -1,7 +1,7 @@ import animeList from '@server/api/animelist'; +import type { TmdbTvDetails } from '@server/api/indexer/themoviedb/interfaces'; import type { PlexLibraryItem, PlexMetadata } from '@server/api/plexapi'; import PlexAPI from '@server/api/plexapi'; -import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; import cacheManager from '@server/lib/cache'; diff --git a/server/lib/scanners/sonarr/index.ts b/server/lib/scanners/sonarr/index.ts index 7a6e95c0..51abe0a0 100644 --- a/server/lib/scanners/sonarr/index.ts +++ b/server/lib/scanners/sonarr/index.ts @@ -1,6 +1,6 @@ +import type { TmdbTvDetails } from '@server/api/indexer/themoviedb/interfaces'; import type { SonarrSeries } from '@server/api/servarr/sonarr'; import SonarrAPI from '@server/api/servarr/sonarr'; -import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; import type { diff --git a/server/lib/search.ts b/server/lib/search.ts index be9ee3ae..68d1743a 100644 --- a/server/lib/search.ts +++ b/server/lib/search.ts @@ -1,4 +1,4 @@ -import TheMovieDb from '@server/api/themoviedb'; +import TheMovieDb from '@server/api/indexer/themoviedb'; import type { TmdbMovieDetails, TmdbMovieResult, @@ -9,7 +9,7 @@ import type { TmdbSearchTvResponse, TmdbTvDetails, TmdbTvResult, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; import { mapMovieDetailsToResult, mapPersonDetailsToResult, diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 52272c74..deb29ccf 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -79,6 +79,12 @@ export interface DVRSettings { overrideRule: number[]; } +export interface TvdbSettings { + apiKey?: string; + pin?: string; + use: boolean; +} + export interface RadarrSettings extends DVRSettings { minimumAvailability: string; } @@ -333,6 +339,7 @@ export interface AllSettings { plex: PlexSettings; jellyfin: JellyfinSettings; tautulli: TautulliSettings; + tvdb: TvdbSettings; radarr: RadarrSettings[]; sonarr: SonarrSettings[]; public: PublicSettings; @@ -399,6 +406,7 @@ class Settings { apiKey: '', }, tautulli: {}, + tvdb: { use: false }, radarr: [], sonarr: [], public: { @@ -593,6 +601,14 @@ class Settings { this.data.tautulli = data; } + get tvdb(): TvdbSettings { + return this.data.tvdb; + } + + set tvdb(data: TvdbSettings) { + this.data.tvdb = data; + } + get radarr(): RadarrSettings[] { return this.data.radarr; } diff --git a/server/models/Collection.ts b/server/models/Collection.ts index 20a3c715..0ac044bb 100644 --- a/server/models/Collection.ts +++ b/server/models/Collection.ts @@ -1,4 +1,4 @@ -import type { TmdbCollection } from '@server/api/themoviedb/interfaces'; +import type { TmdbCollection } from '@server/api/indexer/themoviedb/interfaces'; import { MediaType } from '@server/constants/media'; import type Media from '@server/entity/Media'; import { sortBy } from 'lodash'; diff --git a/server/models/Movie.ts b/server/models/Movie.ts index 87ea7936..59eeb7c7 100644 --- a/server/models/Movie.ts +++ b/server/models/Movie.ts @@ -2,7 +2,7 @@ import type { TmdbMovieDetails, TmdbMovieReleaseResult, TmdbProductionCompany, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; import type Media from '@server/entity/Media'; import type { Cast, diff --git a/server/models/Person.ts b/server/models/Person.ts index 998585ee..f950215b 100644 --- a/server/models/Person.ts +++ b/server/models/Person.ts @@ -2,7 +2,7 @@ import type { TmdbPersonCreditCast, TmdbPersonCreditCrew, TmdbPersonDetails, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; import type Media from '@server/entity/Media'; export interface PersonDetails { diff --git a/server/models/Search.ts b/server/models/Search.ts index 2193bbe1..d4c94b27 100644 --- a/server/models/Search.ts +++ b/server/models/Search.ts @@ -6,7 +6,7 @@ import type { TmdbPersonResult, TmdbTvDetails, TmdbTvResult, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; import { MediaType as MainMediaType } from '@server/constants/media'; import type Media from '@server/entity/Media'; diff --git a/server/models/Tv.ts b/server/models/Tv.ts index c79f9311..b2703e08 100644 --- a/server/models/Tv.ts +++ b/server/models/Tv.ts @@ -5,7 +5,7 @@ import type { TmdbTvEpisodeResult, TmdbTvRatingResult, TmdbTvSeasonResult, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; import type Media from '@server/entity/Media'; import type { Cast, diff --git a/server/models/common.ts b/server/models/common.ts index 30b40d98..40e3a120 100644 --- a/server/models/common.ts +++ b/server/models/common.ts @@ -7,7 +7,7 @@ import type { TmdbVideoResult, TmdbWatchProviderDetails, TmdbWatchProviders, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; import type { Video } from '@server/models/Movie'; export interface ProductionCompany { diff --git a/server/routes/collection.ts b/server/routes/collection.ts index 8b1cd9ef..6cb48d2e 100644 --- a/server/routes/collection.ts +++ b/server/routes/collection.ts @@ -1,4 +1,4 @@ -import TheMovieDb from '@server/api/themoviedb'; +import TheMovieDb from '@server/api/indexer/themoviedb'; import Media from '@server/entity/Media'; import logger from '@server/logger'; import { mapCollection } from '@server/models/Collection'; diff --git a/server/routes/discover.ts b/server/routes/discover.ts index 4fdd1167..7005fb54 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -1,7 +1,7 @@ +import type { SortOptions } from '@server/api/indexer/themoviedb'; +import TheMovieDb from '@server/api/indexer/themoviedb'; +import type { TmdbKeyword } from '@server/api/indexer/themoviedb/interfaces'; import PlexTvAPI from '@server/api/plextv'; -import type { SortOptions } from '@server/api/themoviedb'; -import TheMovieDb from '@server/api/themoviedb'; -import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; diff --git a/server/routes/index.ts b/server/routes/index.ts index b2842139..7439c75f 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,10 +1,10 @@ import GithubAPI from '@server/api/github'; -import PushoverAPI from '@server/api/pushover'; -import TheMovieDb from '@server/api/themoviedb'; +import TheMovieDb from '@server/api/indexer/themoviedb'; import type { TmdbMovieResult, TmdbTvResult, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; +import PushoverAPI from '@server/api/pushover'; import { getRepository } from '@server/datasource'; import DiscoverSlider from '@server/entity/DiscoverSlider'; import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces'; diff --git a/server/routes/media.ts b/server/routes/media.ts index b9983d8b..e4ced854 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -1,7 +1,7 @@ +import TheMovieDb from '@server/api/indexer/themoviedb'; import RadarrAPI from '@server/api/servarr/radarr'; import SonarrAPI from '@server/api/servarr/sonarr'; import TautulliAPI from '@server/api/tautulli'; -import TheMovieDb from '@server/api/themoviedb'; import { MediaStatus, MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; diff --git a/server/routes/movie.ts b/server/routes/movie.ts index 80b8a300..b6432aba 100644 --- a/server/routes/movie.ts +++ b/server/routes/movie.ts @@ -1,7 +1,7 @@ +import TheMovieDb from '@server/api/indexer/themoviedb'; import IMDBRadarrProxy from '@server/api/rating/imdbRadarrProxy'; import RottenTomatoes from '@server/api/rating/rottentomatoes'; import { type RatingResponse } from '@server/api/ratings'; -import TheMovieDb from '@server/api/themoviedb'; import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; diff --git a/server/routes/person.ts b/server/routes/person.ts index 7462328c..5c2a9a47 100644 --- a/server/routes/person.ts +++ b/server/routes/person.ts @@ -1,4 +1,4 @@ -import TheMovieDb from '@server/api/themoviedb'; +import TheMovieDb from '@server/api/indexer/themoviedb'; import Media from '@server/entity/Media'; import logger from '@server/logger'; import { diff --git a/server/routes/search.ts b/server/routes/search.ts index ee2fd9eb..356b9ff8 100644 --- a/server/routes/search.ts +++ b/server/routes/search.ts @@ -1,5 +1,5 @@ -import TheMovieDb from '@server/api/themoviedb'; -import type { TmdbSearchMultiResponse } from '@server/api/themoviedb/interfaces'; +import TheMovieDb from '@server/api/indexer/themoviedb'; +import type { TmdbSearchMultiResponse } from '@server/api/indexer/themoviedb/interfaces'; import Media from '@server/entity/Media'; import { findSearchProvider } from '@server/lib/search'; import logger from '@server/logger'; diff --git a/server/routes/service.ts b/server/routes/service.ts index 8f6c92b0..1c3d645e 100644 --- a/server/routes/service.ts +++ b/server/routes/service.ts @@ -1,6 +1,6 @@ +import TheMovieDb from '@server/api/indexer/themoviedb'; import RadarrAPI from '@server/api/servarr/radarr'; import SonarrAPI from '@server/api/servarr/sonarr'; -import TheMovieDb from '@server/api/themoviedb'; import type { ServiceCommonServer, ServiceCommonServerWithDetails, diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 7298e2c9..f1c1580f 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -42,6 +42,7 @@ import { URL } from 'url'; import notificationRoutes from './notifications'; import radarrRoutes from './radarr'; import sonarrRoutes from './sonarr'; +import tvdbRoutes from './tvdb'; const settingsRoutes = Router(); @@ -49,6 +50,7 @@ settingsRoutes.use('/notifications', notificationRoutes); settingsRoutes.use('/radarr', radarrRoutes); settingsRoutes.use('/sonarr', sonarrRoutes); settingsRoutes.use('/discover', discoverSettingRoutes); +settingsRoutes.use('/tvdb', tvdbRoutes); const filteredMainSettings = ( user: User, diff --git a/server/routes/settings/tvdb.ts b/server/routes/settings/tvdb.ts new file mode 100644 index 00000000..d3b6f341 --- /dev/null +++ b/server/routes/settings/tvdb.ts @@ -0,0 +1,46 @@ +import Tvdb from '@server/api/indexer/tvdb'; +import type { TvdbSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { Router } from 'express'; + +const tvdbRoutes = Router(); + +tvdbRoutes.get('/', (_req, res) => { + const settings = getSettings(); + + res.status(200).json(settings.tvdb); +}); + +tvdbRoutes.put('/', (req, res) => { + const settings = getSettings(); + + const newTvdb = req.body as TvdbSettings; + const tvdb = settings.tvdb; + + tvdb.apiKey = newTvdb.apiKey; + tvdb.pin = newTvdb.pin; + tvdb.use = newTvdb.use; + + settings.tvdb = tvdb; + settings.save(); + + return res.status(200).json(newTvdb); +}); + +tvdbRoutes.post('/test', async (req, res, next) => { + try { + const tvdb = await Tvdb.getInstance(); + await tvdb.login(); + return res.status(200).json({ message: 'Successfully connected to Tvdb' }); + } catch (e) { + logger.error('Failed to test Tvdb', { + label: 'Tvdb', + message: e.message, + }); + + return next({ status: 500, message: 'Failed to connect to Tvdb' }); + } +}); + +export default tvdbRoutes; diff --git a/server/routes/tv.ts b/server/routes/tv.ts index 4a106d60..32619738 100644 --- a/server/routes/tv.ts +++ b/server/routes/tv.ts @@ -1,9 +1,11 @@ +import TheMovieDb from '@server/api/indexer/themoviedb'; +import Tvdb from '@server/api/indexer/tvdb'; import RottenTomatoes from '@server/api/rating/rottentomatoes'; -import TheMovieDb from '@server/api/themoviedb'; import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; import { Watchlist } from '@server/entity/Watchlist'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { mapTvResult } from '@server/models/Search'; import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv'; @@ -12,7 +14,13 @@ import { Router } from 'express'; const tvRoutes = Router(); tvRoutes.get('/:id', async (req, res, next) => { - const tmdb = new TheMovieDb(); + const settings = getSettings(); + let tmdb; + if (settings.tvdb.use) { + tmdb = await Tvdb.getInstance(); + } else { + tmdb = new TheMovieDb(); + } try { const tv = await tmdb.getTvShow({ tvId: Number(req.params.id), @@ -52,14 +60,22 @@ tvRoutes.get('/:id', async (req, res, next) => { } }); -tvRoutes.get('/:id/season/:seasonNumber', async (req, res, next) => { - const tmdb = new TheMovieDb(); - +tvRoutes.get('/:id/season/:seasonNumber/:seasonId', async (req, res, next) => { try { + const settings = getSettings(); + let tmdb; + let seasonIdentifier; + if (settings.tvdb.use) { + tmdb = await Tvdb.getInstance(); + seasonIdentifier = req.params.seasonId; + } else { + tmdb = new TheMovieDb(); + seasonIdentifier = req.params.seasonNumber; + } + const season = await tmdb.getTvSeason({ tvId: Number(req.params.id), - seasonNumber: Number(req.params.seasonNumber), - language: (req.query.language as string) ?? req.locale, + seasonNumber: Number(seasonIdentifier), }); return res.status(200).json(mapSeasonWithEpisodes(season)); diff --git a/server/subscriber/IssueCommentSubscriber.ts b/server/subscriber/IssueCommentSubscriber.ts index 71db981d..c4758dd8 100644 --- a/server/subscriber/IssueCommentSubscriber.ts +++ b/server/subscriber/IssueCommentSubscriber.ts @@ -1,4 +1,4 @@ -import TheMovieDb from '@server/api/themoviedb'; +import TheMovieDb from '@server/api/indexer/themoviedb'; import { IssueType, IssueTypeName } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; diff --git a/server/subscriber/IssueSubscriber.ts b/server/subscriber/IssueSubscriber.ts index d54523cf..bfdd405c 100644 --- a/server/subscriber/IssueSubscriber.ts +++ b/server/subscriber/IssueSubscriber.ts @@ -1,4 +1,4 @@ -import TheMovieDb from '@server/api/themoviedb'; +import TheMovieDb from '@server/api/indexer/themoviedb'; import { IssueStatus, IssueType, IssueTypeName } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; import Issue from '@server/entity/Issue'; diff --git a/server/utils/typeHelpers.ts b/server/utils/typeHelpers.ts index 548378ff..6153ed9c 100644 --- a/server/utils/typeHelpers.ts +++ b/server/utils/typeHelpers.ts @@ -6,7 +6,7 @@ import type { TmdbPersonResult, TmdbTvDetails, TmdbTvResult, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; export const isMovie = ( movie: diff --git a/src/components/Discover/CreateSlider/index.tsx b/src/components/Discover/CreateSlider/index.tsx index 9c7493d2..ba34a12c 100644 --- a/src/components/Discover/CreateSlider/index.tsx +++ b/src/components/Discover/CreateSlider/index.tsx @@ -9,7 +9,7 @@ import type { TmdbCompanySearchResponse, TmdbGenre, TmdbKeywordSearchResponse, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; import { DiscoverSliderType } from '@server/constants/discover'; import type DiscoverSlider from '@server/entity/DiscoverSlider'; import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; diff --git a/src/components/Discover/DiscoverMovieKeyword/index.tsx b/src/components/Discover/DiscoverMovieKeyword/index.tsx index 830b95df..dee8d18c 100644 --- a/src/components/Discover/DiscoverMovieKeyword/index.tsx +++ b/src/components/Discover/DiscoverMovieKeyword/index.tsx @@ -5,7 +5,7 @@ import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover'; import globalMessages from '@app/i18n/globalMessages'; import Error from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; -import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; +import type { TmdbKeyword } from '@server/api/indexer/themoviedb/interfaces'; import type { MovieResult } from '@server/models/Search'; import { useRouter } from 'next/router'; import { useIntl } from 'react-intl'; diff --git a/src/components/Discover/DiscoverMovies/index.tsx b/src/components/Discover/DiscoverMovies/index.tsx index 4f67c205..e8c4f159 100644 --- a/src/components/Discover/DiscoverMovies/index.tsx +++ b/src/components/Discover/DiscoverMovies/index.tsx @@ -13,7 +13,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import Error from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid'; -import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb'; +import type { SortOptions as TMDBSortOptions } from '@server/api/indexer/themoviedb'; import type { MovieResult } from '@server/models/Search'; import { useRouter } from 'next/router'; import { useState } from 'react'; diff --git a/src/components/Discover/DiscoverTv/index.tsx b/src/components/Discover/DiscoverTv/index.tsx index 9d11eeff..0d140b84 100644 --- a/src/components/Discover/DiscoverTv/index.tsx +++ b/src/components/Discover/DiscoverTv/index.tsx @@ -13,7 +13,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import Error from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid'; -import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb'; +import type { SortOptions as TMDBSortOptions } from '@server/api/indexer/themoviedb'; import type { TvResult } from '@server/models/Search'; import { useRouter } from 'next/router'; import { useState } from 'react'; diff --git a/src/components/Discover/DiscoverTvKeyword/index.tsx b/src/components/Discover/DiscoverTvKeyword/index.tsx index a9719fd2..89c45ba1 100644 --- a/src/components/Discover/DiscoverTvKeyword/index.tsx +++ b/src/components/Discover/DiscoverTvKeyword/index.tsx @@ -5,7 +5,7 @@ import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover'; import globalMessages from '@app/i18n/globalMessages'; import Error from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; -import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; +import type { TmdbKeyword } from '@server/api/indexer/themoviedb/interfaces'; import type { TvResult } from '@server/models/Search'; import { useRouter } from 'next/router'; import { useIntl } from 'react-intl'; diff --git a/src/components/GenreTag/index.tsx b/src/components/GenreTag/index.tsx index bbb25afe..ef315e95 100644 --- a/src/components/GenreTag/index.tsx +++ b/src/components/GenreTag/index.tsx @@ -1,7 +1,7 @@ import Spinner from '@app/assets/spinner.svg'; import Tag from '@app/components/Common/Tag'; import { RectangleStackIcon } from '@heroicons/react/24/outline'; -import type { TmdbGenre } from '@server/api/themoviedb/interfaces'; +import type { TmdbGenre } from '@server/api/indexer/themoviedb/interfaces'; import useSWR from 'swr'; type GenreTagProps = { diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 5d2249de..6280593a 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -9,7 +9,7 @@ import useSettings from '@app/hooks/useSettings'; import { useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; -import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; +import { ANIME_KEYWORD_ID } from '@server/api/indexer/themoviedb/constants'; import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; import type { MediaRequest } from '@server/entity/MediaRequest'; import type SeasonRequest from '@server/entity/SeasonRequest'; diff --git a/src/components/Selector/index.tsx b/src/components/Selector/index.tsx index b8d07887..1437a039 100644 --- a/src/components/Selector/index.tsx +++ b/src/components/Selector/index.tsx @@ -11,7 +11,7 @@ import type { TmdbCompanySearchResponse, TmdbGenre, TmdbKeywordSearchResponse, -} from '@server/api/themoviedb/interfaces'; +} from '@server/api/indexer/themoviedb/interfaces'; import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import type { diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx index dd7cd6fa..3d5f5e74 100644 --- a/src/components/Settings/SettingsLayout.tsx +++ b/src/components/Settings/SettingsLayout.tsx @@ -38,6 +38,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => { route: '/settings/users', regex: /^\/settings\/users/, }, + { + text: 'Tvdb', + route: '/settings/tvdb', + regex: /^\/settings\/tvdb/, + }, settings.currentSettings.mediaServerType === MediaServerType.PLEX ? { text: intl.formatMessage(messages.menuPlexSettings), diff --git a/src/components/Settings/SettingsTvdb.tsx b/src/components/Settings/SettingsTvdb.tsx new file mode 100644 index 00000000..b2149e6f --- /dev/null +++ b/src/components/Settings/SettingsTvdb.tsx @@ -0,0 +1,273 @@ +import Button from '@app/components/Common/Button'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import SensitiveInput from '@app/components/Common/SensitiveInput'; +import CopyButton from '@app/components/Settings/CopyButton'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; +import { ArrowPathIcon } from '@heroicons/react/24/solid'; +import type { TvdbSettings } from '@server/lib/settings'; +import { Field, Form, Formik } from 'formik'; +import { useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; + +const messages = defineMessages('components.Settings', { + general: 'General', + settings: 'Settings', + apikey: 'API Key', + pin: 'PIN', + enable: 'Enable', + enableTip: 'Enable Tvdb (only for season and episode)', +}); + +/*interface SettingsTvdbProps { + onEdit: () => void; +}*/ + +const SettingsTvdb = () => { + const intl = useIntl(); + const [isTesting, setIsTesting] = useState(false); + + const { addToast } = useToasts(); + + const testConnection = async (apiKey: string | undefined, pin?: string) => { + const response = await fetch('/api/v1/settings/tvdb/test', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ apiKey, pin }), + }); + + if (!response.ok) { + throw new Error('Failed to test Tvdb'); + } + }; + + const saveSettings = async (values: TvdbSettings) => { + const response = await fetch('/api/v1/settings/tvdb', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(values), + }); + + if (!response.ok) { + throw new Error('Failed to save Tvdb settings'); + } + }; + + const { data, error } = useSWR('/api/v1/settings/tvdb'); + + if (!data && !error) { + return ; + } + + return ( + <> + +
+

{'Tvdb'}

+

{'Settings for Tvdb'}

+
+
+ { + if (values.enable && values.apiKey === '') { + addToast('Please enter an API key', { appearance: 'error' }); + return; + } + + try { + setIsTesting(true); + await testConnection(values.apiKey, values.pin); + setIsTesting(false); + } catch (e) { + addToast('Tvdb connection error, check your credentials', { + appearance: 'error', + }); + return; + } + + try { + await saveSettings({ + apiKey: values.apiKey, + pin: values.pin, + use: values.enable || false, + }); + } catch (e) { + addToast('Failed to save Tvdb settings', { appearance: 'error' }); + return; + } + addToast('Tvdb settings saved', { appearance: 'success' }); + }} + > + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + }) => { + return ( +
+
+ +
+
+ { + setFieldValue('apiKey', e.target.value); + }} + /> + + +
+
+
+ +
+ +
+
+ { + values.pin = e.target.value; + }} + /> + + +
+
+
+ +
+ +
+ { + setFieldValue('enable', !values.enable); + addToast('Tvdb connection successful', { + appearance: 'success', + }); + }} + /> +
+ {errors.apiKey && + touched.apiKey && + typeof errors.apiKey === 'string' && ( +
{errors.apiKey}
+ )} +
+ +
+
+ + + + + + +
+
+
+ ); + }} +
+
+ + ); +}; + +export default SettingsTvdb; diff --git a/src/components/TvDetails/Season/index.tsx b/src/components/TvDetails/Season/index.tsx index 6d929c46..3823ca22 100644 --- a/src/components/TvDetails/Season/index.tsx +++ b/src/components/TvDetails/Season/index.tsx @@ -14,12 +14,13 @@ const messages = defineMessages('components.TvDetails.Season', { type SeasonProps = { seasonNumber: number; tvId: number; + seasonId: number; }; -const Season = ({ seasonNumber, tvId }: SeasonProps) => { +const Season = ({ seasonNumber, tvId, seasonId }: SeasonProps) => { const intl = useIntl(); const { data, error } = useSWR( - `/api/v1/tv/${tvId}/season/${seasonNumber}` + `/api/v1/tv/${tvId}/season/${seasonNumber}/${seasonId}` ); if (!data && !error) { diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 1b008e40..22da2676 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -46,8 +46,8 @@ import { StarIcon, } from '@heroicons/react/24/outline'; import { ChevronDownIcon } from '@heroicons/react/24/solid'; +import { ANIME_KEYWORD_ID } from '@server/api/indexer/themoviedb/constants'; import type { RTRating } from '@server/api/rating/rottentomatoes'; -import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; import { IssueStatus } from '@server/constants/issue'; import { MediaRequestStatus, @@ -1078,6 +1078,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => { diff --git a/src/pages/settings/tvdb.tsx b/src/pages/settings/tvdb.tsx new file mode 100644 index 00000000..e4142d40 --- /dev/null +++ b/src/pages/settings/tvdb.tsx @@ -0,0 +1,16 @@ +import SettingsLayout from '@app/components/Settings/SettingsLayout'; +import SettingsTvdb from '@app/components/Settings/SettingsTvdb'; +import useRouteGuard from '@app/hooks/useRouteGuard'; +import { Permission } from '@app/hooks/useUser'; +import type { NextPage } from 'next'; + +const TvdbSettingsPage: NextPage = () => { + useRouteGuard(Permission.ADMIN); + return ( + + + + ); +}; + +export default TvdbSettingsPage;