Files
jellyseerr/server/routes/tv.ts
THOMAS B 22b2824441 feat: add tvdb indexer (#899)
* feat(tvdb): get tv seasons/episodes with tvdb

* fix: fix rate limiter index tvdb indexer

* fix(usersettings): remove unused column tvdbtoken

* refactor(tvdb): replace tvdb api by skyhook

* fix: error during get episodes

* fix: error if tmdb poster is null

* refactor: clean tvdb indexer code

* fix: wrong language with tmdb indexer

* style: replace avalaible to available

* style: tvdb.login to tvdb.test

* fix(test): fix  discover test

* fix(test): wrong url tv-details

* test(tvdb): add tvdb tests

* style(tvdb): rename pokemon to correct tv show

* refactor(indexer): remove unused getSeasonIdentifier method

* refactor(settings): replace tvdb object to boolean type

* refactor(tmdb): reduce still path condition

* test(tvdb): change 'use' to 'tvdb' condition check

* fix(tmdb): fix build

fix build after rebase

* fix(build): revert package.json

* fix(tvdb): ensure that seasons contain data

* refactor(swagger): fix /tvdb/test response

* fix(scanner): add tvdb indexer for scanner

* refactor(tvdb): remove skyhook api

* refactor(tvdb): use tvdb api

* fix(tvdb): rename tvdb to medatada

* refactor(medata): add tvdb settings

* refactor(metadata): rewrite metadata settings

* refactor(metadata): refactor metadata routes

* refactor(metadata): remove french comments

* refactor(metadata): refactor tvdb api calls

* style(prettier): run prettier

* fix(scanner): fix jellyfin scanner with tvdb provider

* fix(scanner): fix plex scanner tvdb provider

* style(provider): change provider name in info section

* style(provider): full provider name in select

* style(provider): remove french comment

* fix(tests): fix all cypress tests

* refactor(tvdb): fix apikey

* refactor(tmdb): apply prettier

* refactor(tvdb): remove logger info

* feat(metadata): replace fetch with axios for API calls

* feat(provider): replace indexer by provider

* fix(tests): fix cypress test

* chore: add project-wide apikey for tvdb

* chore: add correct application-wide key

* fix(test): fix test with default provider tmdb anime

* style(cypress): fix anime name variable

* chore(i18n): remove french translation + apply i18n:extract

* style(wording): standardize naming to "Metadata Provider" in UI text

* docs(comments): translate from French to English

* refactor(tvdb): remove unnecessary try/catch block

* feat(i18n): add missing translations

* fix(scanner): correct metadata provider ID from Tmdb to Tvdb

* style(settings): clarify navigation label from "Metadata" to "Metadata Providers"

* style(logs): update error log label from "Metadata" to "MetadataProvider"

* refactor(tvdb): replace indexer by metadata providers

* refactor(settings): remove metadata providers logo

* fix(config): restore missing config/db/.gitkeep file

---------

Co-authored-by: TOomaAh <ubuntu@PC>
Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com>
2025-09-02 22:40:47 +02:00

218 lines
6.0 KiB
TypeScript

import { getMetadataProvider } from '@server/api/metadata';
import RottenTomatoes from '@server/api/rating/rottentomatoes';
import TheMovieDb from '@server/api/themoviedb';
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { Watchlist } from '@server/entity/Watchlist';
import logger from '@server/logger';
import { mapTvResult } from '@server/models/Search';
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
import { Router } from 'express';
const tvRoutes = Router();
tvRoutes.get('/:id', async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const tmdbTv = await tmdb.getTvShow({
tvId: Number(req.params.id),
});
const metadataProvider = tmdbTv.keywords.results.some(
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
)
? await getMetadataProvider('anime')
: await getMetadataProvider('tv');
const tv = await metadataProvider.getTvShow({
tvId: Number(req.params.id),
language: (req.query.language as string) ?? req.locale,
});
const media = await Media.getMedia(tv.id, MediaType.TV);
const onUserWatchlist = await getRepository(Watchlist).exist({
where: {
tmdbId: Number(req.params.id),
requestedBy: {
id: req.user?.id,
},
},
});
const data = mapTvDetails(tv, media, onUserWatchlist);
// TMDB issue where it doesnt fallback to English when no overview is available in requested locale.
if (!data.overview) {
const tvEnglish = await metadataProvider.getTvShow({
tvId: Number(req.params.id),
});
data.overview = tvEnglish.overview;
}
return res.status(200).json(data);
} catch (e) {
logger.debug('Something went wrong retrieving series', {
label: 'API',
errorMessage: e.message,
tvId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve series.',
});
}
});
tvRoutes.get('/:id/season/:seasonNumber', async (req, res, next) => {
try {
const tmdb = new TheMovieDb();
const tmdbTv = await tmdb.getTvShow({
tvId: Number(req.params.id),
});
const metadataProvider = tmdbTv.keywords.results.some(
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
)
? await getMetadataProvider('anime')
: await getMetadataProvider('tv');
const season = await metadataProvider.getTvSeason({
tvId: Number(req.params.id),
seasonNumber: Number(req.params.seasonNumber),
});
return res.status(200).json(mapSeasonWithEpisodes(season));
} catch (e) {
logger.debug('Something went wrong retrieving season', {
label: 'API',
errorMessage: e.message,
tvId: req.params.id,
seasonNumber: req.params.seasonNumber,
});
return next({
status: 500,
message: 'Unable to retrieve season.',
});
}
});
tvRoutes.get('/:id/recommendations', async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const results = await tmdb.getTvRecommendations({
tvId: Number(req.params.id),
page: Number(req.query.page),
language: (req.query.language as string) ?? req.locale,
});
const media = await Media.getRelatedMedia(
req.user,
results.results.map((result) => result.id)
);
return res.status(200).json({
page: results.page,
totalPages: results.total_pages,
totalResults: results.total_results,
results: results.results.map((result) =>
mapTvResult(
result,
media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.TV
)
)
),
});
} catch (e) {
logger.debug('Something went wrong retrieving series recommendations', {
label: 'API',
errorMessage: e.message,
tvId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve series recommendations.',
});
}
});
tvRoutes.get('/:id/similar', async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const results = await tmdb.getTvSimilar({
tvId: Number(req.params.id),
page: Number(req.query.page),
language: (req.query.language as string) ?? req.locale,
});
const media = await Media.getRelatedMedia(
req.user,
results.results.map((result) => result.id)
);
return res.status(200).json({
page: results.page,
totalPages: results.total_pages,
totalResults: results.total_results,
results: results.results.map((result) =>
mapTvResult(
result,
media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.TV
)
)
),
});
} catch (e) {
logger.debug('Something went wrong retrieving similar series', {
label: 'API',
errorMessage: e.message,
tvId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve similar series.',
});
}
});
tvRoutes.get('/:id/ratings', async (req, res, next) => {
const tmdb = new TheMovieDb();
const rtapi = new RottenTomatoes();
try {
const tv = await tmdb.getTvShow({
tvId: Number(req.params.id),
});
const rtratings = await rtapi.getTVRatings(
tv.name,
tv.first_air_date ? Number(tv.first_air_date.slice(0, 4)) : undefined
);
if (!rtratings) {
return next({
status: 404,
message: 'Rotten Tomatoes ratings not found.',
});
}
return res.status(200).json(rtratings);
} catch (e) {
logger.debug('Something went wrong retrieving series ratings', {
label: 'API',
errorMessage: e.message,
tvId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve series ratings.',
});
}
});
export default tvRoutes;