mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-28 04:30:34 -05:00
refactor(tvdb): replace tvdb api by skyhook
This commit is contained in:
@@ -522,12 +522,6 @@ components:
|
||||
TvdbSettings:
|
||||
type: object
|
||||
properties:
|
||||
apiKey:
|
||||
type: string
|
||||
example: 'apikey'
|
||||
pin:
|
||||
type: string
|
||||
example: 'ABCDEFGH'
|
||||
use:
|
||||
type: boolean
|
||||
example: true
|
||||
@@ -2617,21 +2611,6 @@ paths:
|
||||
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
|
||||
|
||||
@@ -220,7 +220,7 @@ export interface TmdbTvEpisodeResult {
|
||||
show_id: number;
|
||||
still_path: string;
|
||||
vote_average: number;
|
||||
vote_cuont: number;
|
||||
vote_count: number;
|
||||
}
|
||||
|
||||
export interface TmdbTvSeasonResult {
|
||||
|
||||
@@ -6,26 +6,19 @@ import type {
|
||||
TmdbTvDetails,
|
||||
} from '@server/api/indexer/themoviedb/interfaces';
|
||||
import type {
|
||||
TvdbEpisodeTranslation,
|
||||
TvdbLoginResponse,
|
||||
TvdbSeasonDetails,
|
||||
TvdbTvDetails,
|
||||
TvdbTvShowDetail,
|
||||
} 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) {
|
||||
private constructor() {
|
||||
super(
|
||||
'https://api4.thetvdb.com/v4',
|
||||
{
|
||||
apiKey: apiKey,
|
||||
},
|
||||
'https://skyhook.sonarr.tv/v1/tvdb/shows',
|
||||
{},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('tvdb').data,
|
||||
rateLimit: {
|
||||
@@ -34,16 +27,11 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
|
||||
},
|
||||
}
|
||||
);
|
||||
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);
|
||||
this.instance = new Tvdb();
|
||||
await this.instance.login();
|
||||
logger.info(
|
||||
'Tvdb instance created with token => ' +
|
||||
@@ -55,14 +43,7 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
|
||||
|
||||
async login() {
|
||||
try {
|
||||
const res = await this.post<TvdbLoginResponse>('/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;
|
||||
return await this.get<TvdbLoginResponse>('/en/445009', {});
|
||||
} catch (error) {
|
||||
throw new Error(`[TVDB] Login failed: ${error.message}`);
|
||||
}
|
||||
@@ -85,49 +66,43 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
|
||||
return tmdbTvShow;
|
||||
}
|
||||
|
||||
const data = await this.get<TvdbTvDetails>(
|
||||
`/series/${tvdbId}/extended`,
|
||||
{
|
||||
short: 'true',
|
||||
},
|
||||
const data = await this.get<TvdbTvShowDetail>(
|
||||
`/${language}/${tvdbId}`,
|
||||
{},
|
||||
43200
|
||||
);
|
||||
|
||||
const correctSeasons = data.data.seasons.filter(
|
||||
(season: TvdbSeasonDetails) =>
|
||||
season.id && season.number > 0 && season.type.name === 'Aired Order'
|
||||
);
|
||||
const correctSeasons = data.seasons.filter((value) => {
|
||||
return value.seasonNumber !== 0;
|
||||
});
|
||||
|
||||
tmdbTvShow.seasons = [];
|
||||
|
||||
for (const season of correctSeasons) {
|
||||
if (season.id) {
|
||||
logger.info(`Fetching TV season ${season.id}`);
|
||||
if (season.seasonNumber) {
|
||||
logger.info(`Fetching TV season ${season.seasonNumber}`);
|
||||
|
||||
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,
|
||||
id: tvdbId,
|
||||
episode_count: data.episodes.filter((value) => {
|
||||
return value.seasonNumber === season.seasonNumber;
|
||||
}).length,
|
||||
name: `${season.seasonNumber}`,
|
||||
overview: '',
|
||||
season_number: season.seasonNumber,
|
||||
poster_path: '',
|
||||
air_date: '',
|
||||
image: tvdbSeason.poster_path,
|
||||
image: '',
|
||||
};
|
||||
|
||||
tmdbTvShow.seasons.push(seasonData);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to get season ${season.id} for TV show ${tvdbId}: ${error.message}`,
|
||||
`Failed to get season ${season.seasonNumber} for TV show ${tvdbId}: ${error.message}`,
|
||||
{
|
||||
label: 'Tvdb',
|
||||
message: `Failed to get season ${season.id} for TV show ${tvdbId}`,
|
||||
message: `Failed to get season ${season.seasonNumber} for TV show ${tvdbId}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -142,25 +117,6 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
|
||||
}
|
||||
};
|
||||
|
||||
getEpisode = async (
|
||||
episodeId: number,
|
||||
language: string
|
||||
): Promise<TvdbEpisodeTranslation> => {
|
||||
try {
|
||||
const tvdbEpisode = await this.get<TvdbEpisodeTranslation>(
|
||||
`/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,
|
||||
@@ -184,36 +140,40 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
|
||||
};
|
||||
}
|
||||
try {
|
||||
const tvdbSeason = await this.get<TvdbSeasonDetails>(
|
||||
`/seasons/${seasonNumber}/extended`,
|
||||
const tvdbSeason = await this.get<TvdbTvShowDetail>(
|
||||
`/en/${tvId}`,
|
||||
{ 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,
|
||||
}));
|
||||
const episodes = tvdbSeason.episodes
|
||||
.filter((value) => {
|
||||
return value.seasonNumber === seasonNumber;
|
||||
})
|
||||
.map((episode) => ({
|
||||
id: episode.tvdbId,
|
||||
air_date: episode.airDate,
|
||||
episode_number: episode.episodeNumber,
|
||||
name: episode.title || '',
|
||||
overview: episode.overview || '',
|
||||
season_number: episode.seasonNumber,
|
||||
production_code: '',
|
||||
show_id: tvId,
|
||||
still_path: episode.image || '',
|
||||
vote_average: 1,
|
||||
vote_count: 1,
|
||||
}));
|
||||
|
||||
return {
|
||||
episodes: episodes,
|
||||
external_ids: {
|
||||
tvdb_id: tvdbSeason.seriesId,
|
||||
tvdb_id: tvdbSeason.tvdbId,
|
||||
},
|
||||
name: '',
|
||||
overview: '',
|
||||
id: tvdbSeason.id,
|
||||
air_date: tvdbSeason.year,
|
||||
season_number: tvdbSeason.number,
|
||||
id: tvdbSeason.tvdbId,
|
||||
air_date: tvdbSeason.firstAired,
|
||||
season_number: episodes.length,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,138 +1,75 @@
|
||||
export interface TvdbBaseResponse<T> {
|
||||
data: T;
|
||||
errors: any;
|
||||
errors: string;
|
||||
}
|
||||
|
||||
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<TvdbTvDetails> {
|
||||
id: number;
|
||||
name: string;
|
||||
export interface TvdbTvShowDetail {
|
||||
tvdbId: number;
|
||||
title: string;
|
||||
overview: 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;
|
||||
firstAired: string;
|
||||
lastAired: string;
|
||||
tvMazeId: number;
|
||||
tmdbId: number;
|
||||
imdbId: string;
|
||||
lastUpdated: string;
|
||||
status: 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;
|
||||
timeOfDay: TvdbTimeOfDay;
|
||||
originalNetwork: string;
|
||||
network: string;
|
||||
genres: string[];
|
||||
alternativeTitles: TvdbAlternativeTitle[];
|
||||
actors: TvdbActor[];
|
||||
images: TvdbImage[];
|
||||
seasons: TvdbSeason[];
|
||||
episodes: TvdbEpisode[];
|
||||
}
|
||||
|
||||
export interface TvdbSeasonDetails extends TvdbBaseResponse<TvdbSeasonDetails> {
|
||||
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 TvdbTimeOfDay {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
}
|
||||
|
||||
export interface TvdbAlternativeTitle {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface TvdbActor {
|
||||
name: string;
|
||||
character: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface TvdbImage {
|
||||
coverType: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface TvdbSeason {
|
||||
seasonNumber: number;
|
||||
}
|
||||
|
||||
export interface TvdbEpisode {
|
||||
tvdbShowId: number;
|
||||
tvdbId: number;
|
||||
seasonNumber: number;
|
||||
episodeNumber: number;
|
||||
absoluteEpisodeNumber: number;
|
||||
title?: string;
|
||||
airDate: string;
|
||||
airDateUtc: string;
|
||||
runtime?: number;
|
||||
overview?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface TvdbEpisodeTranslation
|
||||
|
||||
@@ -80,8 +80,6 @@ export interface DVRSettings {
|
||||
}
|
||||
|
||||
export interface TvdbSettings {
|
||||
apiKey?: string;
|
||||
pin?: string;
|
||||
use: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
||||
seasonNumber: episode.season_number,
|
||||
showId: episode.show_id,
|
||||
voteAverage: episode.vote_average,
|
||||
voteCount: episode.vote_cuont,
|
||||
voteCount: episode.vote_count,
|
||||
stillPath: episode.still_path,
|
||||
});
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ tvdbRoutes.put('/', (req, res) => {
|
||||
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;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
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';
|
||||
@@ -33,13 +30,12 @@ const SettingsTvdb = () => {
|
||||
|
||||
const { addToast } = useToasts();
|
||||
|
||||
const testConnection = async (apiKey: string | undefined, pin?: string) => {
|
||||
const testConnection = async () => {
|
||||
const response = await fetch('/api/v1/settings/tvdb/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ apiKey, pin }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -82,19 +78,12 @@ const SettingsTvdb = () => {
|
||||
<div className="section">
|
||||
<Formik
|
||||
initialValues={{
|
||||
apiKey: data?.apiKey,
|
||||
pin: data?.pin,
|
||||
enable: data?.use,
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
if (values.enable && values.apiKey === '') {
|
||||
addToast('Please enter an API key', { appearance: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsTesting(true);
|
||||
await testConnection(values.apiKey, values.pin);
|
||||
await testConnection();
|
||||
setIsTesting(false);
|
||||
} catch (e) {
|
||||
addToast('Tvdb connection error, check your credentials', {
|
||||
@@ -105,8 +94,6 @@ const SettingsTvdb = () => {
|
||||
|
||||
try {
|
||||
await saveSettings({
|
||||
apiKey: values.apiKey,
|
||||
pin: values.pin,
|
||||
use: values.enable || false,
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -116,75 +103,9 @@ const SettingsTvdb = () => {
|
||||
addToast('Tvdb settings saved', { appearance: 'success' });
|
||||
}}
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
}) => {
|
||||
{({ isSubmitting, isValid, values, setFieldValue }) => {
|
||||
return (
|
||||
<Form className="section" data-testid="settings-main-form">
|
||||
<div className="form-row">
|
||||
<label htmlFor="apiKey" className="text-label">
|
||||
{intl.formatMessage(messages.apikey)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<SensitiveInput
|
||||
type="text"
|
||||
id="apiKey"
|
||||
className="rounded-l-only"
|
||||
value={values.apiKey}
|
||||
onChange={(e) => {
|
||||
setFieldValue('apiKey', e.target.value);
|
||||
}}
|
||||
/>
|
||||
<CopyButton
|
||||
textToCopy={values.apiKey ?? ''}
|
||||
key={'apikey'}
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
className="input-action"
|
||||
>
|
||||
<ArrowPathIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label htmlFor="pin" className="text-label">
|
||||
{intl.formatMessage(messages.pin)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<SensitiveInput
|
||||
type="text"
|
||||
id="pin"
|
||||
className="rounded-l-only"
|
||||
value={values.pin}
|
||||
onChange={(e) => {
|
||||
values.pin = e.target.value;
|
||||
}}
|
||||
/>
|
||||
<CopyButton textToCopy={values.pin ?? ''} key={'pin'} />
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
className="input-action"
|
||||
>
|
||||
<ArrowPathIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label htmlFor="trustProxy" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
@@ -207,11 +128,7 @@ const SettingsTvdb = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.apiKey &&
|
||||
touched.apiKey &&
|
||||
typeof errors.apiKey === 'string' && (
|
||||
<div className="error">{errors.apiKey}</div>
|
||||
)}
|
||||
<div className="error"></div>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
@@ -224,7 +141,7 @@ const SettingsTvdb = () => {
|
||||
onClick={async () => {
|
||||
setIsTesting(true);
|
||||
try {
|
||||
await testConnection(values.apiKey, values.pin);
|
||||
await testConnection();
|
||||
addToast('Tvdb connection successful', {
|
||||
appearance: 'success',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user