Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tvdb indexer #899

Open
wants to merge 29 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4baa0e6
feat(tvdb): get tv seasons/episodes with tvdb
TOomaAh Jul 26, 2024
5911ad5
fix: fix rate limiter index tvdb indexer
TOomaAh Jul 26, 2024
7e2ec24
fix(usersettings): remove unused column tvdbtoken
TOomaAh Jul 26, 2024
3137703
refactor(tvdb): replace tvdb api by skyhook
TOomaAh Oct 19, 2024
f8e8c29
fix: error during get episodes
TOomaAh Oct 20, 2024
a5406e7
fix: error if tmdb poster is null
TOomaAh Oct 20, 2024
c84ca3d
refactor: clean tvdb indexer code
TOomaAh Oct 22, 2024
ad42c4e
fix: wrong language with tmdb indexer
TOomaAh Oct 23, 2024
79fa6a3
style: replace avalaible to available
TOomaAh Oct 26, 2024
41b471d
style: tvdb.login to tvdb.test
TOomaAh Oct 26, 2024
204ecf1
fix(test): fix discover test
TOomaAh Oct 26, 2024
501eac9
fix(test): wrong url tv-details
TOomaAh Oct 26, 2024
51de966
test(tvdb): add tvdb tests
TOomaAh Oct 27, 2024
b90ffc6
style(tvdb): rename pokemon to correct tv show
TOomaAh Oct 27, 2024
2193755
refactor(indexer): remove unused getSeasonIdentifier method
TOomaAh Oct 28, 2024
b325805
refactor(settings): replace tvdb object to boolean type
TOomaAh Oct 29, 2024
f37312c
refactor(tmdb): reduce still path condition
TOomaAh Oct 29, 2024
b84dd14
test(tvdb): change 'use' to 'tvdb' condition check
TOomaAh Oct 29, 2024
40fabcc
fix(tmdb): fix build
TOomaAh Dec 12, 2024
619087c
fix(build): revert package.json
TOomaAh Dec 12, 2024
4336230
fix(tvdb): ensure that seasons contain data
TOomaAh Dec 12, 2024
c6f2cc1
refactor(swagger): fix /tvdb/test response
TOomaAh Dec 12, 2024
03f97bd
fix(scanner): add tvdb indexer for scanner
TOomaAh Jan 8, 2025
353d079
style(mediarequest): apply prettier
fallenbagel Jan 15, 2025
973e8d1
fix(build): remove circular dependency
TOomaAh Jan 15, 2025
a54c360
refactor(tvdb): remove skyhook api
TOomaAh Jan 19, 2025
c5d77a0
refactor(tvdb): use tvdb api
TOomaAh Jan 20, 2025
76ae619
fix(tvdb): rename tvdb to medatada
TOomaAh Jan 20, 2025
211a57a
refactor(medata): add tvdb settings
TOomaAh Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions cypress/e2e/indexers/tvdb.cy.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
80 changes: 77 additions & 3 deletions overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,32 @@ components:
serverID:
type: string
readOnly: true
MetadataSettings:
type: object
properties:
settings:
type: object
properties:
tv:
type: string
enum: [tvdb, tmdb]
example: 'tvdb'
anime:
type: string
enum: [tvdb, tmdb]
example: 'tvdb'
providers:
type: object
properties:
tvdb:
type: object
properties:
apiKey:
type: string
example: '123456789'
pin:
type: string
example: '1234'
TautulliSettings:
type: object
properties:
Expand Down Expand Up @@ -2378,6 +2404,54 @@ paths:
type: string
thumb:
type: string
/settings/metadatas:
get:
summary: Get Metadata settings
description: Retrieves current Metadata settings.
tags:
- settings
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/MetadataSettings'
put:
summary: Update Metadata settings
description: Updates Metadata settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MetadataSettings'
responses:
'200':
description: 'Values were successfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/MetadataSettings'
/settings/metadatas/test:
post:
summary: Test Provider 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:
message:
type: string
example: 'Successfully connected to TVDB'
/settings/tautulli:
get:
summary: Get Tautulli settings
Expand Down Expand Up @@ -5953,7 +6027,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/TvDetails'
/tv/{tvId}/season/{seasonId}:
/tv/{tvId}/season/{seasonNumber}:
get:
summary: Get season details and episode list
description: Returns season details with a list of episodes in a JSON object.
Expand All @@ -5967,11 +6041,11 @@ paths:
type: number
example: 76479
- in: path
name: seasonId
name: seasonNumber
required: true
schema:
type: number
example: 1
example: 123456
- in: query
name: language
schema:
Expand Down
10 changes: 2 additions & 8 deletions server/api/externalapi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { MediaServerType } from '@server/constants/server';
import { getSettings } from '@server/lib/settings';
import type { RateLimitOptions } from '@server/utils/rateLimit';
import rateLimit from '@server/utils/rateLimit';
import type NodeCache from 'node-cache';
Expand All @@ -10,7 +8,7 @@ const DEFAULT_TTL = 300;
// 10 seconds default rolling buffer (in ms)
const DEFAULT_ROLLING_BUFFER = 10000;

interface ExternalAPIOptions {
export interface ExternalAPIOptions {
nodeCache?: NodeCache;
headers?: Record<string, unknown>;
rateLimit?: RateLimitOptions;
Expand All @@ -36,8 +34,6 @@ class ExternalAPI {

const url = new URL(baseUrl);

const settings = getSettings();

this.defaultHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json',
Expand All @@ -46,9 +42,7 @@ class ExternalAPI {
`${url.username}:${url.password}`
).toString('base64')}`,
}),
...(settings.main.mediaServerType === MediaServerType.EMBY && {
'Accept-Encoding': 'gzip',
}),

...options.headers,
};

Expand Down
30 changes: 30 additions & 0 deletions server/api/indexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {
TmdbSeasonWithEpisodes,
TmdbTvDetails,
} from '@server/api/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>;
getShowByTvdbId({
tvdbId,
language,
}: {
tvdbId: number;
language?: string;
}): Promise<TmdbTvDetails>;
}
7 changes: 7 additions & 0 deletions server/api/jellyfin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ExternalAPI from '@server/api/externalapi';
import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType } from '@server/constants/server';
import availabilitySync from '@server/lib/availabilitySync';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { ApiError } from '@server/types/error';
import { getAppVersion } from '@server/utils/appVersion';
Expand Down Expand Up @@ -103,12 +105,17 @@ class JellyfinAPI extends ExternalAPI {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}"`;
}

const settings = getSettings();

super(
jellyfinHost,
{},
{
headers: {
'X-Emby-Authorization': authHeaderVal,
...(settings.main.mediaServerType === MediaServerType.EMBY && {
'Accept-Encoding': 'gzip',
}),
},
}
);
Expand Down
36 changes: 36 additions & 0 deletions server/api/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { TvShowIndexer } from '@server/api/indexer';
import TheMovieDb from '@server/api/themoviedb';
import Tvdb from '@server/api/tvdb';
import { getSettings, IndexerType } from '@server/lib/settings';
import logger from '@server/logger';

export const getMetadataProvider = async (
mediaType: 'movie' | 'tv' | 'anime'
): Promise<TvShowIndexer> => {
try {
const settings = await getSettings();

if (!settings.tvdb.apiKey || mediaType == 'movie') {
return new TheMovieDb();
}

if (mediaType == 'tv' && settings.metadataType.tv == IndexerType.TVDB) {
return await Tvdb.getInstance();
}

if (
mediaType == 'anime' &&
settings.metadataType.anime == IndexerType.TVDB
) {
return await Tvdb.getInstance();
}

return new TheMovieDb();
} catch (e) {
logger.error('Failed to get metadata provider', {
label: 'Metadata',
message: e.message,
});
return new TheMovieDb();
}
};
10 changes: 9 additions & 1 deletion server/api/themoviedb/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ExternalAPI from '@server/api/externalapi';
import type { TvShowIndexer } from '@server/api/indexer';
import cacheManager from '@server/lib/cache';
import { sortBy } from 'lodash';
import type {
Expand Down Expand Up @@ -98,7 +99,7 @@ interface DiscoverTvOptions {
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 discoverRegion?: string;
private originalLanguage?: string;
constructor({
Expand Down Expand Up @@ -308,6 +309,13 @@ 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;
} catch (e) {
throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`);
Expand Down
2 changes: 1 addition & 1 deletion server/api/themoviedb/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading