refactor(tvdb): replace tvdb api by skyhook

This commit is contained in:
TOomaAh
2024-10-20 01:46:08 +02:00
parent aa7de132be
commit b88606a1df
8 changed files with 111 additions and 322 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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

View File

@@ -80,8 +80,6 @@ export interface DVRSettings {
}
export interface TvdbSettings {
apiKey?: string;
pin?: string;
use: boolean;
}

View File

@@ -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,
});

View File

@@ -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;

View File

@@ -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',
});