Compare commits
20 Commits
test-exter
...
preview-tv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ee5535819 | ||
|
|
67a846cd58 | ||
|
|
a5e8320e8a | ||
|
|
3f16176667 | ||
|
|
85aeeb084e | ||
|
|
b865d65fad | ||
|
|
47eece9c44 | ||
|
|
72277ea983 | ||
|
|
57e2f7b374 | ||
|
|
6f8d4bf00a | ||
|
|
61ecf74b28 | ||
|
|
976781d470 | ||
|
|
422012f7b5 | ||
|
|
32f500a4e7 | ||
|
|
7b07004c5b | ||
|
|
87253e8bb7 | ||
|
|
7bcda9521e | ||
|
|
2d51b16694 | ||
|
|
2a0bcdf41c | ||
|
|
79e542ef12 |
92
cypress/e2e/indexers/tvdb.cy.ts
Normal file
92
cypress/e2e/indexers/tvdb.cy.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
describe('TVDB Integration', () => {
|
||||||
|
// Constants for routes and selectors
|
||||||
|
const ROUTES = {
|
||||||
|
home: '/',
|
||||||
|
tvdbSettings: '/settings/tvdb',
|
||||||
|
tomorrowIsOursTvShow: '/tv/72879',
|
||||||
|
monsterTvShow: '/tv/225634',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SELECTORS = {
|
||||||
|
sidebarToggle: '[data-testid=sidebar-toggle]',
|
||||||
|
sidebarSettingsMobile: '[data-testid=sidebar-menu-settings-mobile]',
|
||||||
|
settingsNavDesktop: 'nav[data-testid="settings-nav-desktop"]',
|
||||||
|
tvdbEnable: 'input[data-testid="tvdb-enable"]',
|
||||||
|
tvdbSaveButton: '[data-testid=tvbd-save-button]',
|
||||||
|
heading: '.heading',
|
||||||
|
season1: 'Season 1',
|
||||||
|
season2: 'Season 2',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reusable commands
|
||||||
|
const toggleTVDBSetting = () => {
|
||||||
|
cy.intercept('/api/v1/settings/tvdb').as('tvdbRequest');
|
||||||
|
cy.get(SELECTORS.tvdbSaveButton).click();
|
||||||
|
return cy.wait('@tvdbRequest');
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyTVDBResponse = (response, expectedUseValue) => {
|
||||||
|
expect(response.statusCode).to.equal(200);
|
||||||
|
expect(response.body.tvdb).to.equal(expectedUseValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Perform login
|
||||||
|
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
|
||||||
|
|
||||||
|
// Navigate to TVDB settings
|
||||||
|
cy.visit(ROUTES.home);
|
||||||
|
cy.get(SELECTORS.sidebarToggle).click();
|
||||||
|
cy.get(SELECTORS.sidebarSettingsMobile).click();
|
||||||
|
cy.get(
|
||||||
|
`${SELECTORS.settingsNavDesktop} a[href="${ROUTES.tvdbSettings}"]`
|
||||||
|
).click();
|
||||||
|
|
||||||
|
// Verify heading
|
||||||
|
cy.get(SELECTORS.heading).should('contain', 'Tvdb');
|
||||||
|
|
||||||
|
// Configure TVDB settings
|
||||||
|
cy.get(SELECTORS.tvdbEnable).then(($checkbox) => {
|
||||||
|
const isChecked = $checkbox.is(':checked');
|
||||||
|
|
||||||
|
if (!isChecked) {
|
||||||
|
// If disabled, enable TVDB
|
||||||
|
cy.wrap($checkbox).click();
|
||||||
|
toggleTVDBSetting().then(({ response }) => {
|
||||||
|
verifyTVDBResponse(response, true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If enabled, disable then re-enable TVDB
|
||||||
|
cy.wrap($checkbox).click();
|
||||||
|
toggleTVDBSetting().then(({ response }) => {
|
||||||
|
verifyTVDBResponse(response, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.wrap($checkbox).click();
|
||||||
|
toggleTVDBSetting().then(({ response }) => {
|
||||||
|
verifyTVDBResponse(response, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display "Tomorrow is Ours" show information correctly (1 season on TMDB >1 seasons on TVDB)', () => {
|
||||||
|
cy.visit(ROUTES.tomorrowIsOursTvShow);
|
||||||
|
cy.contains(SELECTORS.season2)
|
||||||
|
.should('be.visible')
|
||||||
|
.scrollIntoView()
|
||||||
|
.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should display "Monster" show information correctly (Not existing on TVDB)', () => {
|
||||||
|
cy.visit(ROUTES.monsterTvShow);
|
||||||
|
cy.intercept('/api/v1/tv/225634/season/1').as('season1');
|
||||||
|
cy.contains(SELECTORS.season1)
|
||||||
|
.should('be.visible')
|
||||||
|
.scrollIntoView()
|
||||||
|
.click();
|
||||||
|
cy.wait('@season1');
|
||||||
|
|
||||||
|
cy.contains('9 - Hang Men').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,6 +10,7 @@ module.exports = {
|
|||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{ hostname: 'gravatar.com' },
|
{ hostname: 'gravatar.com' },
|
||||||
{ hostname: 'image.tmdb.org' },
|
{ hostname: 'image.tmdb.org' },
|
||||||
|
{ hostname: 'artworks.thetvdb.com' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
|
|||||||
@@ -400,6 +400,12 @@ components:
|
|||||||
serverID:
|
serverID:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
TvdbSettings:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
use:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
TautulliSettings:
|
TautulliSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -2361,6 +2367,60 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
thumb:
|
thumb:
|
||||||
type: string
|
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
|
||||||
|
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:
|
/settings/tautulli:
|
||||||
get:
|
get:
|
||||||
summary: Get Tautulli settings
|
summary: Get Tautulli settings
|
||||||
@@ -5909,7 +5969,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/TvDetails'
|
$ref: '#/components/schemas/TvDetails'
|
||||||
/tv/{tvId}/season/{seasonId}:
|
/tv/{tvId}/season/{seasonNumber}:
|
||||||
get:
|
get:
|
||||||
summary: Get season details and episode list
|
summary: Get season details and episode list
|
||||||
description: Returns season details with a list of episodes in a JSON object.
|
description: Returns season details with a list of episodes in a JSON object.
|
||||||
@@ -5923,11 +5983,11 @@ paths:
|
|||||||
type: number
|
type: number
|
||||||
example: 76479
|
example: 76479
|
||||||
- in: path
|
- in: path
|
||||||
name: seasonId
|
name: seasonNumber
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: number
|
type: number
|
||||||
example: 1
|
example: 123456
|
||||||
- in: query
|
- in: query
|
||||||
name: language
|
name: language
|
||||||
schema:
|
schema:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const DEFAULT_TTL = 300;
|
|||||||
// 10 seconds default rolling buffer (in ms)
|
// 10 seconds default rolling buffer (in ms)
|
||||||
const DEFAULT_ROLLING_BUFFER = 10000;
|
const DEFAULT_ROLLING_BUFFER = 10000;
|
||||||
|
|
||||||
interface ExternalAPIOptions {
|
export interface ExternalAPIOptions {
|
||||||
nodeCache?: NodeCache;
|
nodeCache?: NodeCache;
|
||||||
headers?: Record<string, unknown>;
|
headers?: Record<string, unknown>;
|
||||||
rateLimit?: RateLimitOptions;
|
rateLimit?: RateLimitOptions;
|
||||||
@@ -53,6 +53,7 @@ class ExternalAPI {
|
|||||||
|
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
this.params = params;
|
this.params = params;
|
||||||
|
|
||||||
this.cache = options.nodeCache;
|
this.cache = options.nodeCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
server/api/indexer/index.ts
Normal file
23
server/api/indexer/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type {
|
||||||
|
TmdbSeasonWithEpisodes,
|
||||||
|
TmdbTvDetails,
|
||||||
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
|
|
||||||
|
export interface TvShowIndexer {
|
||||||
|
getTvShow({
|
||||||
|
tvId,
|
||||||
|
language,
|
||||||
|
}: {
|
||||||
|
tvId: number;
|
||||||
|
language?: string;
|
||||||
|
}): Promise<TmdbTvDetails>;
|
||||||
|
getTvSeason({
|
||||||
|
tvId,
|
||||||
|
seasonNumber,
|
||||||
|
language,
|
||||||
|
}: {
|
||||||
|
tvId: number;
|
||||||
|
seasonNumber: number;
|
||||||
|
language?: string;
|
||||||
|
}): Promise<TmdbSeasonWithEpisodes>;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import ExternalAPI from '@server/api/externalapi';
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
|
import type { TvShowIndexer } from '@server/api/indexer';
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import type {
|
import type {
|
||||||
@@ -98,7 +99,7 @@ interface DiscoverTvOptions {
|
|||||||
withStatus?: string; // Returning Series: 0 Planned: 1 In Production: 2 Ended: 3 Cancelled: 4 Pilot: 5
|
withStatus?: string; // Returning Series: 0 Planned: 1 In Production: 2 Ended: 3 Cancelled: 4 Pilot: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
class TheMovieDb extends ExternalAPI {
|
class TheMovieDb extends ExternalAPI implements TvShowIndexer {
|
||||||
private region?: string;
|
private region?: string;
|
||||||
private originalLanguage?: string;
|
private originalLanguage?: string;
|
||||||
constructor({
|
constructor({
|
||||||
@@ -308,6 +309,12 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
data.episodes = data.episodes.map((episode) => {
|
||||||
|
if (episode.still_path) {
|
||||||
|
episode.still_path = `https://image.tmdb.org/t/p/original/${episode.still_path}`;
|
||||||
|
}
|
||||||
|
return episode;
|
||||||
|
});
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`);
|
||||||
@@ -220,7 +220,7 @@ export interface TmdbTvEpisodeResult {
|
|||||||
show_id: number;
|
show_id: number;
|
||||||
still_path: string;
|
still_path: string;
|
||||||
vote_average: number;
|
vote_average: number;
|
||||||
vote_cuont: number;
|
vote_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbTvSeasonResult {
|
export interface TmdbTvSeasonResult {
|
||||||
246
server/api/indexer/tvdb/index.ts
Normal file
246
server/api/indexer/tvdb/index.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
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 {
|
||||||
|
TvdbEpisode,
|
||||||
|
TvdbLoginResponse,
|
||||||
|
TvdbSeason,
|
||||||
|
TvdbTvShowDetail,
|
||||||
|
} from '@server/api/indexer/tvdb/interfaces';
|
||||||
|
import cacheManager, { type AvailableCacheIds } from '@server/lib/cache';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
|
||||||
|
interface TvdbConfig {
|
||||||
|
baseUrl: string;
|
||||||
|
maxRequestsPerSecond: number;
|
||||||
|
cachePrefix: AvailableCacheIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: TvdbConfig = {
|
||||||
|
baseUrl: 'https://skyhook.sonarr.tv/v1/tvdb/shows',
|
||||||
|
maxRequestsPerSecond: 50,
|
||||||
|
cachePrefix: 'tvdb' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const enum TvdbIdStatus {
|
||||||
|
INVALID = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
type TvdbId = number;
|
||||||
|
type ValidTvdbId = Exclude<TvdbId, TvdbIdStatus.INVALID>;
|
||||||
|
|
||||||
|
class Tvdb extends ExternalAPI implements TvShowIndexer {
|
||||||
|
private readonly tmdb: TheMovieDb;
|
||||||
|
private static readonly DEFAULT_CACHE_TTL = 43200;
|
||||||
|
private static readonly DEFAULT_LANGUAGE = 'en';
|
||||||
|
|
||||||
|
constructor(config: Partial<TvdbConfig> = {}) {
|
||||||
|
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
||||||
|
|
||||||
|
super(
|
||||||
|
finalConfig.baseUrl,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
nodeCache: cacheManager.getCache(finalConfig.cachePrefix).data,
|
||||||
|
rateLimit: {
|
||||||
|
maxRPS: finalConfig.maxRequestsPerSecond,
|
||||||
|
id: finalConfig.cachePrefix,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.tmdb = new TheMovieDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async test(): Promise<TvdbLoginResponse> {
|
||||||
|
try {
|
||||||
|
return await this.get<TvdbLoginResponse>('/en/445009', {});
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError('Login failed', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTvShow({
|
||||||
|
tvId,
|
||||||
|
language = Tvdb.DEFAULT_LANGUAGE,
|
||||||
|
}: {
|
||||||
|
tvId: number;
|
||||||
|
language?: string;
|
||||||
|
}): Promise<TmdbTvDetails> {
|
||||||
|
try {
|
||||||
|
const tmdbTvShow = await this.tmdb.getTvShow({ tvId, language });
|
||||||
|
const tvdbId = this.getTvdbIdFromTmdb(tmdbTvShow);
|
||||||
|
|
||||||
|
if (this.isValidTvdbId(tvdbId)) {
|
||||||
|
return await this.enrichTmdbShowWithTvdbData(tmdbTvShow, tvdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmdbTvShow;
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError('Failed to fetch TV show details', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTvSeason({
|
||||||
|
tvId,
|
||||||
|
seasonNumber,
|
||||||
|
language = Tvdb.DEFAULT_LANGUAGE,
|
||||||
|
}: {
|
||||||
|
tvId: number;
|
||||||
|
seasonNumber: number;
|
||||||
|
language?: string;
|
||||||
|
}): Promise<TmdbSeasonWithEpisodes> {
|
||||||
|
if (seasonNumber === 0) {
|
||||||
|
return this.createEmptySeasonResponse(tvId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tmdbTvShow = await this.tmdb.getTvShow({ tvId, language });
|
||||||
|
const tvdbId = this.getTvdbIdFromTmdb(tmdbTvShow);
|
||||||
|
|
||||||
|
if (!this.isValidTvdbId(tvdbId)) {
|
||||||
|
return await this.tmdb.getTvSeason({ tvId, seasonNumber, language });
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.getTvdbSeasonData(tvdbId, seasonNumber, tvId);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[TVDB] Failed to fetch TV season details: ${error.message}`
|
||||||
|
);
|
||||||
|
return await this.tmdb.getTvSeason({ tvId, seasonNumber, language });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async enrichTmdbShowWithTvdbData(
|
||||||
|
tmdbTvShow: TmdbTvDetails,
|
||||||
|
tvdbId: ValidTvdbId
|
||||||
|
): Promise<TmdbTvDetails> {
|
||||||
|
try {
|
||||||
|
const tvdbData = await this.fetchTvdbShowData(tvdbId);
|
||||||
|
const seasons = this.processSeasons(tvdbData);
|
||||||
|
return { ...tmdbTvShow, seasons };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to enrich TMDB show with TVDB data: ${error.message}`
|
||||||
|
);
|
||||||
|
return tmdbTvShow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchTvdbShowData(tvdbId: number): Promise<TvdbTvShowDetail> {
|
||||||
|
return await this.get<TvdbTvShowDetail>(
|
||||||
|
`/en/${tvdbId}`,
|
||||||
|
{},
|
||||||
|
Tvdb.DEFAULT_CACHE_TTL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processSeasons(tvdbData: TvdbTvShowDetail): any[] {
|
||||||
|
return tvdbData.seasons
|
||||||
|
.filter((season) => season.seasonNumber !== 0)
|
||||||
|
.map((season) => this.createSeasonData(season, tvdbData));
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSeasonData(
|
||||||
|
season: TvdbSeason,
|
||||||
|
tvdbData: TvdbTvShowDetail
|
||||||
|
): any {
|
||||||
|
if (!season.seasonNumber) return null;
|
||||||
|
|
||||||
|
const episodeCount = tvdbData.episodes.filter(
|
||||||
|
(episode) => episode.seasonNumber === season.seasonNumber
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: tvdbData.tvdbId,
|
||||||
|
episode_count: episodeCount,
|
||||||
|
name: `${season.seasonNumber}`,
|
||||||
|
overview: '',
|
||||||
|
season_number: season.seasonNumber,
|
||||||
|
poster_path: '',
|
||||||
|
air_date: '',
|
||||||
|
image: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTvdbSeasonData(
|
||||||
|
tvdbId: number,
|
||||||
|
seasonNumber: number,
|
||||||
|
tvId: number
|
||||||
|
): Promise<TmdbSeasonWithEpisodes> {
|
||||||
|
const tvdbSeason = await this.fetchTvdbShowData(tvdbId);
|
||||||
|
|
||||||
|
const episodes = this.processEpisodes(tvdbSeason, seasonNumber, tvId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
episodes,
|
||||||
|
external_ids: { tvdb_id: tvdbSeason.tvdbId },
|
||||||
|
name: '',
|
||||||
|
overview: '',
|
||||||
|
id: tvdbSeason.tvdbId,
|
||||||
|
air_date: tvdbSeason.firstAired,
|
||||||
|
season_number: episodes.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private processEpisodes(
|
||||||
|
tvdbSeason: TvdbTvShowDetail,
|
||||||
|
seasonNumber: number,
|
||||||
|
tvId: number
|
||||||
|
): any[] {
|
||||||
|
return tvdbSeason.episodes
|
||||||
|
.filter((episode) => episode.seasonNumber === seasonNumber)
|
||||||
|
.map((episode, index) => this.createEpisodeData(episode, index, tvId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private createEpisodeData(
|
||||||
|
episode: TvdbEpisode,
|
||||||
|
index: number,
|
||||||
|
tvId: number
|
||||||
|
): any {
|
||||||
|
return {
|
||||||
|
id: episode.tvdbId,
|
||||||
|
air_date: episode.airDate,
|
||||||
|
episode_number: episode.episodeNumber,
|
||||||
|
name: episode.title || `Episode ${index + 1}`,
|
||||||
|
overview: episode.overview || '',
|
||||||
|
season_number: episode.seasonNumber,
|
||||||
|
production_code: '',
|
||||||
|
show_id: tvId,
|
||||||
|
still_path: episode.image || '',
|
||||||
|
vote_average: 1,
|
||||||
|
vote_count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private createEmptySeasonResponse(tvId: number): TmdbSeasonWithEpisodes {
|
||||||
|
return {
|
||||||
|
episodes: [],
|
||||||
|
external_ids: { tvdb_id: tvId },
|
||||||
|
name: '',
|
||||||
|
overview: '',
|
||||||
|
id: 0,
|
||||||
|
air_date: '',
|
||||||
|
season_number: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTvdbIdFromTmdb(tmdbTvShow: TmdbTvDetails): TvdbId {
|
||||||
|
return tmdbTvShow?.external_ids?.tvdb_id ?? TvdbIdStatus.INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidTvdbId(tvdbId: TvdbId): tvdbId is ValidTvdbId {
|
||||||
|
return tvdbId !== TvdbIdStatus.INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(context: string, error: Error): void {
|
||||||
|
throw new Error(`[TVDB] ${context}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tvdb;
|
||||||
80
server/api/indexer/tvdb/interfaces.ts
Normal file
80
server/api/indexer/tvdb/interfaces.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
export interface TvdbBaseResponse<T> {
|
||||||
|
data: T;
|
||||||
|
errors: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TvdbLoginResponse extends TvdbBaseResponse<{ token: string }> {
|
||||||
|
data: { token: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TvdbTvShowDetail {
|
||||||
|
tvdbId: number;
|
||||||
|
title: string;
|
||||||
|
overview: string;
|
||||||
|
slug: string;
|
||||||
|
originalCountry: string;
|
||||||
|
originalLanguage: string;
|
||||||
|
language: string;
|
||||||
|
firstAired: string;
|
||||||
|
lastAired: string;
|
||||||
|
tvMazeId: number;
|
||||||
|
tmdbId: number;
|
||||||
|
imdbId: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
status: string;
|
||||||
|
runtime: number;
|
||||||
|
timeOfDay: TvdbTimeOfDay;
|
||||||
|
originalNetwork: string;
|
||||||
|
network: string;
|
||||||
|
genres: string[];
|
||||||
|
alternativeTitles: TvdbAlternativeTitle[];
|
||||||
|
actors: TvdbActor[];
|
||||||
|
images: TvdbImage[];
|
||||||
|
seasons: TvdbSeason[];
|
||||||
|
episodes: TvdbEpisode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
extends TvdbBaseResponse<TvdbEpisodeTranslation> {
|
||||||
|
name: string;
|
||||||
|
overview: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
|
import { ANIME_KEYWORD_ID } from '@server/api/indexer/themoviedb/constants';
|
||||||
import type { RadarrMovieOptions } from '@server/api/servarr/radarr';
|
import type { RadarrMovieOptions } from '@server/api/servarr/radarr';
|
||||||
import RadarrAPI from '@server/api/servarr/radarr';
|
import RadarrAPI from '@server/api/servarr/radarr';
|
||||||
import type {
|
import type {
|
||||||
@@ -5,8 +7,6 @@ import type {
|
|||||||
SonarrSeries,
|
SonarrSeries,
|
||||||
} from '@server/api/servarr/sonarr';
|
} from '@server/api/servarr/sonarr';
|
||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
|
||||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export type AvailableCacheIds =
|
|||||||
| 'imdb'
|
| 'imdb'
|
||||||
| 'github'
|
| 'github'
|
||||||
| 'plexguid'
|
| 'plexguid'
|
||||||
| 'plextv';
|
| 'plextv'
|
||||||
|
| 'tvdb';
|
||||||
|
|
||||||
const DEFAULT_TTL = 300;
|
const DEFAULT_TTL = 300;
|
||||||
const DEFAULT_CHECK_PERIOD = 120;
|
const DEFAULT_CHECK_PERIOD = 120;
|
||||||
@@ -68,6 +69,10 @@ class CacheManager {
|
|||||||
stdTtl: 86400 * 7, // 1 week cache
|
stdTtl: 86400 * 7, // 1 week cache
|
||||||
checkPeriod: 60,
|
checkPeriod: 60,
|
||||||
}),
|
}),
|
||||||
|
tvdb: new Cache('tvdb', 'The TVDB API', {
|
||||||
|
stdTtl: 21600,
|
||||||
|
checkPeriod: 60 * 30,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public getCache(id: AvailableCacheIds): Cache {
|
public getCache(id: AvailableCacheIds): Cache {
|
||||||
|
|||||||
@@ -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 { MediaStatus, MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
|
|||||||
@@ -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 type { JellyfinLibraryItem } from '@server/api/jellyfin';
|
||||||
import JellyfinAPI 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 { MediaStatus, MediaType } from '@server/constants/media';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import animeList from '@server/api/animelist';
|
import animeList from '@server/api/animelist';
|
||||||
|
import type { TmdbTvDetails } from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import type { PlexLibraryItem, PlexMetadata } from '@server/api/plexapi';
|
import type { PlexLibraryItem, PlexMetadata } from '@server/api/plexapi';
|
||||||
import PlexAPI from '@server/api/plexapi';
|
import PlexAPI from '@server/api/plexapi';
|
||||||
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
|
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import { User } from '@server/entity/User';
|
import { User } from '@server/entity/User';
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import type { TmdbTvDetails } from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import type { SonarrSeries } from '@server/api/servarr/sonarr';
|
import type { SonarrSeries } from '@server/api/servarr/sonarr';
|
||||||
import SonarrAPI 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 { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import type {
|
import type {
|
||||||
TmdbMovieDetails,
|
TmdbMovieDetails,
|
||||||
TmdbMovieResult,
|
TmdbMovieResult,
|
||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
TmdbSearchTvResponse,
|
TmdbSearchTvResponse,
|
||||||
TmdbTvDetails,
|
TmdbTvDetails,
|
||||||
TmdbTvResult,
|
TmdbTvResult,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import {
|
import {
|
||||||
mapMovieDetailsToResult,
|
mapMovieDetailsToResult,
|
||||||
mapPersonDetailsToResult,
|
mapPersonDetailsToResult,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import type { TvShowIndexer } from '@server/api/indexer';
|
||||||
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
|
import Tvdb from '@server/api/indexer/tvdb';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { runMigrations } from '@server/lib/settings/migrator';
|
import { runMigrations } from '@server/lib/settings/migrator';
|
||||||
@@ -303,6 +306,7 @@ export interface AllSettings {
|
|||||||
public: PublicSettings;
|
public: PublicSettings;
|
||||||
notifications: NotificationSettings;
|
notifications: NotificationSettings;
|
||||||
jobs: Record<JobId, JobSettings>;
|
jobs: Record<JobId, JobSettings>;
|
||||||
|
tvdb: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
||||||
@@ -368,6 +372,7 @@ class Settings {
|
|||||||
apiKey: '',
|
apiKey: '',
|
||||||
},
|
},
|
||||||
tautulli: {},
|
tautulli: {},
|
||||||
|
tvdb: false,
|
||||||
radarr: [],
|
radarr: [],
|
||||||
sonarr: [],
|
sonarr: [],
|
||||||
public: {
|
public: {
|
||||||
@@ -532,6 +537,14 @@ class Settings {
|
|||||||
this.data.tautulli = data;
|
this.data.tautulli = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get tvdb(): boolean {
|
||||||
|
return this.data.tvdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
set tvdb(data: boolean) {
|
||||||
|
this.data.tvdb = data;
|
||||||
|
}
|
||||||
|
|
||||||
get radarr(): RadarrSettings[] {
|
get radarr(): RadarrSettings[] {
|
||||||
return this.data.radarr;
|
return this.data.radarr;
|
||||||
}
|
}
|
||||||
@@ -699,4 +712,13 @@ export const getSettings = (initialSettings?: AllSettings): Settings => {
|
|||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getIndexer = (): TvShowIndexer => {
|
||||||
|
const settings = getSettings();
|
||||||
|
if (settings.tvdb) {
|
||||||
|
return new Tvdb();
|
||||||
|
} else {
|
||||||
|
return new TheMovieDb();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
|||||||
@@ -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 { MediaType } from '@server/constants/media';
|
||||||
import type Media from '@server/entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
TmdbMovieDetails,
|
TmdbMovieDetails,
|
||||||
TmdbMovieReleaseResult,
|
TmdbMovieReleaseResult,
|
||||||
TmdbProductionCompany,
|
TmdbProductionCompany,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import type Media from '@server/entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
import type {
|
import type {
|
||||||
Cast,
|
Cast,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
TmdbPersonCreditCast,
|
TmdbPersonCreditCast,
|
||||||
TmdbPersonCreditCrew,
|
TmdbPersonCreditCrew,
|
||||||
TmdbPersonDetails,
|
TmdbPersonDetails,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import type Media from '@server/entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
|
|
||||||
export interface PersonDetails {
|
export interface PersonDetails {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
TmdbPersonResult,
|
TmdbPersonResult,
|
||||||
TmdbTvDetails,
|
TmdbTvDetails,
|
||||||
TmdbTvResult,
|
TmdbTvResult,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import { MediaType as MainMediaType } from '@server/constants/media';
|
import { MediaType as MainMediaType } from '@server/constants/media';
|
||||||
import type Media from '@server/entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type {
|
|||||||
TmdbTvEpisodeResult,
|
TmdbTvEpisodeResult,
|
||||||
TmdbTvRatingResult,
|
TmdbTvRatingResult,
|
||||||
TmdbTvSeasonResult,
|
TmdbTvSeasonResult,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import type Media from '@server/entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
import type {
|
import type {
|
||||||
Cast,
|
Cast,
|
||||||
@@ -124,7 +124,7 @@ const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
|||||||
seasonNumber: episode.season_number,
|
seasonNumber: episode.season_number,
|
||||||
showId: episode.show_id,
|
showId: episode.show_id,
|
||||||
voteAverage: episode.vote_average,
|
voteAverage: episode.vote_average,
|
||||||
voteCount: episode.vote_cuont,
|
voteCount: episode.vote_count,
|
||||||
stillPath: episode.still_path,
|
stillPath: episode.still_path,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
TmdbVideoResult,
|
TmdbVideoResult,
|
||||||
TmdbWatchProviderDetails,
|
TmdbWatchProviderDetails,
|
||||||
TmdbWatchProviders,
|
TmdbWatchProviders,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import type { Video } from '@server/models/Movie';
|
import type { Video } from '@server/models/Movie';
|
||||||
|
|
||||||
export interface ProductionCompany {
|
export interface ProductionCompany {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { mapCollection } from '@server/models/Collection';
|
import { mapCollection } from '@server/models/Collection';
|
||||||
|
|||||||
@@ -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 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 { MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import GithubAPI from '@server/api/github';
|
import GithubAPI from '@server/api/github';
|
||||||
import PushoverAPI from '@server/api/pushover';
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
|
||||||
import type {
|
import type {
|
||||||
TmdbMovieResult,
|
TmdbMovieResult,
|
||||||
TmdbTvResult,
|
TmdbTvResult,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
|
import PushoverAPI from '@server/api/pushover';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||||
import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces';
|
import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import RadarrAPI from '@server/api/servarr/radarr';
|
import RadarrAPI from '@server/api/servarr/radarr';
|
||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
import TautulliAPI from '@server/api/tautulli';
|
import TautulliAPI from '@server/api/tautulli';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
|
||||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import IMDBRadarrProxy from '@server/api/rating/imdbRadarrProxy';
|
import IMDBRadarrProxy from '@server/api/rating/imdbRadarrProxy';
|
||||||
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
||||||
import { type RatingResponse } from '@server/api/ratings';
|
import { type RatingResponse } from '@server/api/ratings';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import type { TmdbSearchMultiResponse } from '@server/api/themoviedb/interfaces';
|
import type { TmdbSearchMultiResponse } from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
import { findSearchProvider } from '@server/lib/search';
|
import { findSearchProvider } from '@server/lib/search';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import RadarrAPI from '@server/api/servarr/radarr';
|
import RadarrAPI from '@server/api/servarr/radarr';
|
||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
|
||||||
import type {
|
import type {
|
||||||
ServiceCommonServer,
|
ServiceCommonServer,
|
||||||
ServiceCommonServerWithDetails,
|
ServiceCommonServerWithDetails,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { URL } from 'url';
|
|||||||
import notificationRoutes from './notifications';
|
import notificationRoutes from './notifications';
|
||||||
import radarrRoutes from './radarr';
|
import radarrRoutes from './radarr';
|
||||||
import sonarrRoutes from './sonarr';
|
import sonarrRoutes from './sonarr';
|
||||||
|
import tvdbRoutes from './tvdb';
|
||||||
|
|
||||||
const settingsRoutes = Router();
|
const settingsRoutes = Router();
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ settingsRoutes.use('/notifications', notificationRoutes);
|
|||||||
settingsRoutes.use('/radarr', radarrRoutes);
|
settingsRoutes.use('/radarr', radarrRoutes);
|
||||||
settingsRoutes.use('/sonarr', sonarrRoutes);
|
settingsRoutes.use('/sonarr', sonarrRoutes);
|
||||||
settingsRoutes.use('/discover', discoverSettingRoutes);
|
settingsRoutes.use('/discover', discoverSettingRoutes);
|
||||||
|
settingsRoutes.use('/tvdb', tvdbRoutes);
|
||||||
|
|
||||||
const filteredMainSettings = (
|
const filteredMainSettings = (
|
||||||
user: User,
|
user: User,
|
||||||
|
|||||||
48
server/routes/settings/tvdb.ts
Normal file
48
server/routes/settings/tvdb.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import Tvdb from '@server/api/indexer/tvdb';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
const tvdbRoutes = Router();
|
||||||
|
|
||||||
|
export interface TvdbSettings {
|
||||||
|
tvdb: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
tvdbRoutes.get('/', (_req, res) => {
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
tvdb: settings.tvdb,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tvdbRoutes.put('/', (req, res) => {
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
const body = req.body as TvdbSettings;
|
||||||
|
|
||||||
|
settings.tvdb = body.tvdb ?? settings.tvdb ?? false;
|
||||||
|
settings.save();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
tvdb: settings.tvdb,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tvdbRoutes.post('/test', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const tvdb = new Tvdb();
|
||||||
|
await tvdb.test();
|
||||||
|
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;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
import { Watchlist } from '@server/entity/Watchlist';
|
import { Watchlist } from '@server/entity/Watchlist';
|
||||||
|
import { getIndexer } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { mapTvResult } from '@server/models/Search';
|
import { mapTvResult } from '@server/models/Search';
|
||||||
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
|
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
|
||||||
@@ -12,9 +13,10 @@ import { Router } from 'express';
|
|||||||
const tvRoutes = Router();
|
const tvRoutes = Router();
|
||||||
|
|
||||||
tvRoutes.get('/:id', async (req, res, next) => {
|
tvRoutes.get('/:id', async (req, res, next) => {
|
||||||
const tmdb = new TheMovieDb();
|
const indexer = getIndexer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tv = await tmdb.getTvShow({
|
const tv = await indexer.getTvShow({
|
||||||
tvId: Number(req.params.id),
|
tvId: Number(req.params.id),
|
||||||
language: (req.query.language as string) ?? req.locale,
|
language: (req.query.language as string) ?? req.locale,
|
||||||
});
|
});
|
||||||
@@ -34,7 +36,9 @@ tvRoutes.get('/:id', async (req, res, next) => {
|
|||||||
|
|
||||||
// TMDB issue where it doesnt fallback to English when no overview is available in requested locale.
|
// TMDB issue where it doesnt fallback to English when no overview is available in requested locale.
|
||||||
if (!data.overview) {
|
if (!data.overview) {
|
||||||
const tvEnglish = await tmdb.getTvShow({ tvId: Number(req.params.id) });
|
const tvEnglish = await indexer.getTvShow({
|
||||||
|
tvId: Number(req.params.id),
|
||||||
|
});
|
||||||
data.overview = tvEnglish.overview;
|
data.overview = tvEnglish.overview;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +57,12 @@ tvRoutes.get('/:id', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tvRoutes.get('/:id/season/:seasonNumber', async (req, res, next) => {
|
tvRoutes.get('/:id/season/:seasonNumber', async (req, res, next) => {
|
||||||
const tmdb = new TheMovieDb();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const season = await tmdb.getTvSeason({
|
const indexer = getIndexer();
|
||||||
|
|
||||||
|
const season = await indexer.getTvSeason({
|
||||||
tvId: Number(req.params.id),
|
tvId: Number(req.params.id),
|
||||||
seasonNumber: Number(req.params.seasonNumber),
|
seasonNumber: Number(req.params.seasonNumber),
|
||||||
language: (req.query.language as string) ?? req.locale,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json(mapSeasonWithEpisodes(season));
|
return res.status(200).json(mapSeasonWithEpisodes(season));
|
||||||
|
|||||||
@@ -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 { IssueType, IssueTypeName } from '@server/constants/issue';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
|
|||||||
@@ -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 { IssueStatus, IssueType, IssueTypeName } from '@server/constants/issue';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import Issue from '@server/entity/Issue';
|
import Issue from '@server/entity/Issue';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/indexer/themoviedb';
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
TmdbPersonResult,
|
TmdbPersonResult,
|
||||||
TmdbTvDetails,
|
TmdbTvDetails,
|
||||||
TmdbTvResult,
|
TmdbTvResult,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
|
|
||||||
export const isMovie = (
|
export const isMovie = (
|
||||||
movie:
|
movie:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
TmdbCompanySearchResponse,
|
TmdbCompanySearchResponse,
|
||||||
TmdbGenre,
|
TmdbGenre,
|
||||||
TmdbKeywordSearchResponse,
|
TmdbKeywordSearchResponse,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import { DiscoverSliderType } from '@server/constants/discover';
|
import { DiscoverSliderType } from '@server/constants/discover';
|
||||||
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import Error from '@app/pages/_error';
|
import Error from '@app/pages/_error';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
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 type { MovieResult } from '@server/models/Search';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
|||||||
import Error from '@app/pages/_error';
|
import Error from '@app/pages/_error';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
|
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 type { MovieResult } from '@server/models/Search';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
|||||||
import Error from '@app/pages/_error';
|
import Error from '@app/pages/_error';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
|
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 type { TvResult } from '@server/models/Search';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import Error from '@app/pages/_error';
|
import Error from '@app/pages/_error';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
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 type { TvResult } from '@server/models/Search';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Spinner from '@app/assets/spinner.svg';
|
import Spinner from '@app/assets/spinner.svg';
|
||||||
import Tag from '@app/components/Common/Tag';
|
import Tag from '@app/components/Common/Tag';
|
||||||
import { RectangleStackIcon } from '@heroicons/react/24/outline';
|
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';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
type GenreTagProps = {
|
type GenreTagProps = {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import useSettings from '@app/hooks/useSettings';
|
|||||||
import { useUser } from '@app/hooks/useUser';
|
import { useUser } from '@app/hooks/useUser';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
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 { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type SeasonRequest from '@server/entity/SeasonRequest';
|
import type SeasonRequest from '@server/entity/SeasonRequest';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
TmdbCompanySearchResponse,
|
TmdbCompanySearchResponse,
|
||||||
TmdbGenre,
|
TmdbGenre,
|
||||||
TmdbKeywordSearchResponse,
|
TmdbKeywordSearchResponse,
|
||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/indexer/themoviedb/interfaces';
|
||||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||||
import type {
|
import type {
|
||||||
Keyword,
|
Keyword,
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => {
|
|||||||
route: '/settings/users',
|
route: '/settings/users',
|
||||||
regex: /^\/settings\/users/,
|
regex: /^\/settings\/users/,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Tvdb',
|
||||||
|
route: '/settings/tvdb',
|
||||||
|
regex: /^\/settings\/tvdb/,
|
||||||
|
},
|
||||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX
|
settings.currentSettings.mediaServerType === MediaServerType.PLEX
|
||||||
? {
|
? {
|
||||||
text: intl.formatMessage(messages.menuPlexSettings),
|
text: intl.formatMessage(messages.menuPlexSettings),
|
||||||
|
|||||||
198
src/components/Settings/SettingsTvdb.tsx
Normal file
198
src/components/Settings/SettingsTvdb.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import Button from '@app/components/Common/Button';
|
||||||
|
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||||
|
import PageTitle from '@app/components/Common/PageTitle';
|
||||||
|
import SettingsBadge from '@app/components/Settings/SettingsBadge';
|
||||||
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import type { TvdbSettings } from '@server/routes/settings/tvdb';
|
||||||
|
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).' +
|
||||||
|
' Due to a limitation of the api used, only English is available.',
|
||||||
|
});
|
||||||
|
|
||||||
|
const SettingsTvdb = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
|
||||||
|
const testConnection = async () => {
|
||||||
|
const response = await fetch('/api/v1/settings/tvdb/test', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to test Tvdb');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSettings = async (value: TvdbSettings) => {
|
||||||
|
const response = await fetch('/api/v1/settings/tvdb', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
tvdb: value.tvdb,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save Tvdb settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error } = useSWR<TvdbSettings>('/api/v1/settings/tvdb');
|
||||||
|
|
||||||
|
if (!data && !error) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle
|
||||||
|
title={[
|
||||||
|
intl.formatMessage(messages.general),
|
||||||
|
intl.formatMessage(globalMessages.settings),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="heading">{'Tvdb'}</h3>
|
||||||
|
<p className="description">{'Settings for Tvdb'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="section">
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
enable: data?.tvdb ?? false,
|
||||||
|
}}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
try {
|
||||||
|
setIsTesting(true);
|
||||||
|
await testConnection();
|
||||||
|
setIsTesting(false);
|
||||||
|
} catch (e) {
|
||||||
|
addToast('Tvdb connection error, check your credentials', {
|
||||||
|
appearance: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveSettings({
|
||||||
|
tvdb: values.enable ?? false,
|
||||||
|
});
|
||||||
|
if (data) {
|
||||||
|
data.tvdb = values.enable;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addToast('Failed to save Tvdb settings', { appearance: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addToast('Tvdb settings saved', { appearance: 'success' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, isValid, values, setFieldValue }) => {
|
||||||
|
return (
|
||||||
|
<Form className="section" data-testid="settings-main-form">
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="trustProxy" className="checkbox-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.enable)}
|
||||||
|
</span>
|
||||||
|
<SettingsBadge badgeType="experimental" />
|
||||||
|
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.enableTip)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Field
|
||||||
|
data-testid="tvdb-enable"
|
||||||
|
type="checkbox"
|
||||||
|
id="enable"
|
||||||
|
name="enable"
|
||||||
|
onChange={() => {
|
||||||
|
setFieldValue('enable', !values.enable);
|
||||||
|
addToast('Tvdb connection successful', {
|
||||||
|
appearance: 'success',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="error"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="actions">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="warning"
|
||||||
|
type="button"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsTesting(true);
|
||||||
|
try {
|
||||||
|
await testConnection();
|
||||||
|
addToast('Tvdb connection successful', {
|
||||||
|
appearance: 'success',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addToast(
|
||||||
|
'Tvdb connection error, check your credentials',
|
||||||
|
{ appearance: 'error' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setIsTesting(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BeakerIcon />
|
||||||
|
<span>
|
||||||
|
{isTesting
|
||||||
|
? intl.formatMessage(globalMessages.testing)
|
||||||
|
: intl.formatMessage(globalMessages.test)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
data-testid="tvbd-save-button"
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
|
<ArrowDownOnSquareIcon />
|
||||||
|
<span>
|
||||||
|
{isSubmitting
|
||||||
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
: intl.formatMessage(globalMessages.save)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsTvdb;
|
||||||
@@ -59,7 +59,7 @@ const Season = ({ seasonNumber, tvId }: SeasonProps) => {
|
|||||||
<div className="relative aspect-video xl:h-32">
|
<div className="relative aspect-video xl:h-32">
|
||||||
<Image
|
<Image
|
||||||
className="rounded-lg object-contain"
|
className="rounded-lg object-contain"
|
||||||
src={`https://image.tmdb.org/t/p/original/${episode.stillPath}`}
|
src={episode.stillPath}
|
||||||
alt=""
|
alt=""
|
||||||
fill
|
fill
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ import {
|
|||||||
MinusCircleIcon,
|
MinusCircleIcon,
|
||||||
StarIcon,
|
StarIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
|
import { ANIME_KEYWORD_ID } from '@server/api/indexer/themoviedb/constants';
|
||||||
import type { RTRating } from '@server/api/rating/rottentomatoes';
|
import type { RTRating } from '@server/api/rating/rottentomatoes';
|
||||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
|
||||||
import { IssueStatus } from '@server/constants/issue';
|
import { IssueStatus } from '@server/constants/issue';
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
@@ -119,9 +119,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const [showRequestModal, setShowRequestModal] = useState(false);
|
const [showRequestModal, setShowRequestModal] = useState(false);
|
||||||
const [showManager, setShowManager] = useState(
|
const [showManager, setShowManager] = useState(router.query.manage == '1');
|
||||||
router.query.manage == '1' ? true : false
|
|
||||||
);
|
|
||||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
|
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
|
||||||
@@ -157,7 +155,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowManager(router.query.manage == '1' ? true : false);
|
setShowManager(router.query.manage == '1');
|
||||||
}, [router.query.manage]);
|
}, [router.query.manage]);
|
||||||
|
|
||||||
const closeBlacklistModal = useCallback(
|
const closeBlacklistModal = useCallback(
|
||||||
@@ -189,7 +187,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
mediaLinks.push({
|
mediaLinks.push({
|
||||||
text: getAvalaibleMediaServerName(),
|
text: getAvailableMediaServerName(),
|
||||||
url: plexUrl,
|
url: plexUrl,
|
||||||
svg: <PlayIcon />,
|
svg: <PlayIcon />,
|
||||||
});
|
});
|
||||||
@@ -203,7 +201,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
mediaLinks.push({
|
mediaLinks.push({
|
||||||
text: getAvalaible4kMediaServerName(),
|
text: getAvailable4kMediaServerName(),
|
||||||
url: plexUrl4k,
|
url: plexUrl4k,
|
||||||
svg: <PlayIcon />,
|
svg: <PlayIcon />,
|
||||||
});
|
});
|
||||||
@@ -307,7 +305,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
|
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
|
||||||
?.flatrate ?? [];
|
?.flatrate ?? [];
|
||||||
|
|
||||||
function getAvalaibleMediaServerName() {
|
function getAvailableMediaServerName() {
|
||||||
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
||||||
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
|
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
|
||||||
}
|
}
|
||||||
@@ -319,7 +317,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' });
|
return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvalaible4kMediaServerName() {
|
function getAvailable4kMediaServerName() {
|
||||||
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
||||||
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
|
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/pages/settings/tvdb.tsx
Normal file
16
src/pages/settings/tvdb.tsx
Normal file
@@ -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 (
|
||||||
|
<SettingsLayout>
|
||||||
|
<SettingsTvdb />
|
||||||
|
</SettingsLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TvdbSettingsPage;
|
||||||
Reference in New Issue
Block a user