mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
refactor: clean tvdb indexer code
This commit is contained in:
@@ -6,188 +6,254 @@ import type {
|
||||
TmdbTvDetails,
|
||||
} from '@server/api/indexer/themoviedb/interfaces';
|
||||
import type {
|
||||
TvdbEpisode,
|
||||
TvdbLoginResponse,
|
||||
TvdbSeason,
|
||||
TvdbTvShowDetail,
|
||||
} from '@server/api/indexer/tvdb/interfaces';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import cacheManager, { type AvailableCacheIds } from '@server/lib/cache';
|
||||
import logger from '@server/logger';
|
||||
|
||||
class Tvdb extends ExternalAPI implements TvShowIndexer {
|
||||
private tmdb: TheMovieDb = new TheMovieDb();
|
||||
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 };
|
||||
|
||||
public constructor() {
|
||||
super(
|
||||
'https://skyhook.sonarr.tv/v1/tvdb/shows',
|
||||
finalConfig.baseUrl,
|
||||
{},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('tvdb').data,
|
||||
nodeCache: cacheManager.getCache(finalConfig.cachePrefix).data,
|
||||
rateLimit: {
|
||||
maxRPS: 50,
|
||||
id: 'tvdb',
|
||||
maxRPS: finalConfig.maxRequestsPerSecond,
|
||||
id: finalConfig.cachePrefix,
|
||||
},
|
||||
}
|
||||
);
|
||||
this.tmdb = new TheMovieDb();
|
||||
}
|
||||
|
||||
async login() {
|
||||
public async login(): Promise<TvdbLoginResponse> {
|
||||
try {
|
||||
return await this.get<TvdbLoginResponse>('/en/445009', {});
|
||||
} catch (error) {
|
||||
throw new Error(`[TVDB] Login failed: ${error.message}`);
|
||||
this.handleError('Login failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public getTvShow = async ({
|
||||
public async getTvShow({
|
||||
tvId,
|
||||
language = Tvdb.DEFAULT_LANGUAGE,
|
||||
}: {
|
||||
tvId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbTvDetails> => {
|
||||
}): Promise<TmdbTvDetails> {
|
||||
try {
|
||||
const tmdbTvShow = await this.tmdb.getTvShow({ tvId: tvId });
|
||||
const tmdbTvShow = await this.tmdb.getTvShow({ tvId });
|
||||
const tvdbId = this.getTvdbIdFromTmdb(tmdbTvShow);
|
||||
|
||||
if (tvdbId === -1) {
|
||||
return tmdbTvShow;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await this.get<TvdbTvShowDetail>(
|
||||
`/en/${tvdbId}`,
|
||||
{},
|
||||
43200
|
||||
if (this.isValidTvdbId(tvdbId)) {
|
||||
return await this.enrichTmdbShowWithTvdbData(
|
||||
tmdbTvShow,
|
||||
tvdbId,
|
||||
language
|
||||
);
|
||||
|
||||
const correctSeasons = data.seasons.filter((value) => {
|
||||
return value.seasonNumber !== 0;
|
||||
});
|
||||
|
||||
tmdbTvShow.seasons = [];
|
||||
|
||||
for (const season of correctSeasons) {
|
||||
if (season.seasonNumber) {
|
||||
try {
|
||||
const seasonData = {
|
||||
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: '',
|
||||
};
|
||||
|
||||
tmdbTvShow.seasons.push(seasonData);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to get season ${season.seasonNumber} for TV show ${tvdbId}: ${error.message}`,
|
||||
{
|
||||
label: 'Tvdb',
|
||||
message: `Failed to get season ${season.seasonNumber} for TV show ${tvdbId}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tmdbTvShow;
|
||||
} catch (e) {
|
||||
return tmdbTvShow;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`[TVDB] Failed to fetch TV show details: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private getTvdbIdFromTmdb(tmdbTvShow: TmdbTvDetails) {
|
||||
try {
|
||||
return tmdbTvShow.external_ids.tvdb_id || -1;
|
||||
} catch (e) {
|
||||
return -1;
|
||||
return tmdbTvShow;
|
||||
} catch (error) {
|
||||
this.handleError('Failed to fetch TV show details', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public getTvSeason = async ({
|
||||
public async getTvSeason({
|
||||
tvId,
|
||||
seasonNumber,
|
||||
language = 'en',
|
||||
language = Tvdb.DEFAULT_LANGUAGE,
|
||||
}: {
|
||||
tvId: number;
|
||||
seasonNumber: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSeasonWithEpisodes> => {
|
||||
}): Promise<TmdbSeasonWithEpisodes> {
|
||||
if (seasonNumber === 0) {
|
||||
return {
|
||||
episodes: [],
|
||||
external_ids: {
|
||||
tvdb_id: tvId,
|
||||
},
|
||||
name: '',
|
||||
overview: '',
|
||||
id: seasonNumber,
|
||||
air_date: '',
|
||||
season_number: 0,
|
||||
};
|
||||
return this.createEmptySeasonResponse(tvId);
|
||||
}
|
||||
|
||||
const tmdbTvShow = await this.tmdb.getTvShow({ tvId: tvId });
|
||||
const tvdbId = this.getTvdbIdFromTmdb(tmdbTvShow);
|
||||
|
||||
if (tvdbId === -1) {
|
||||
return await this.tmdb.getTvSeason({ tvId, seasonNumber, language });
|
||||
}
|
||||
try {
|
||||
const tvdbSeason = await this.get<TvdbTvShowDetail>(
|
||||
`/en/${tvdbId}`,
|
||||
{ lang: language },
|
||||
43200
|
||||
);
|
||||
const tmdbTvShow = await this.tmdb.getTvShow({ tvId });
|
||||
const tvdbId = this.getTvdbIdFromTmdb(tmdbTvShow);
|
||||
|
||||
const episodes = tvdbSeason.episodes
|
||||
.filter((value) => {
|
||||
return value.seasonNumber === seasonNumber;
|
||||
})
|
||||
.map((episode, index) => ({
|
||||
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,
|
||||
}));
|
||||
if (!this.isValidTvdbId(tvdbId)) {
|
||||
return await this.tmdb.getTvSeason({ tvId, seasonNumber, language });
|
||||
}
|
||||
|
||||
return {
|
||||
episodes: episodes,
|
||||
external_ids: {
|
||||
tvdb_id: tvdbSeason.tvdbId,
|
||||
},
|
||||
name: '',
|
||||
overview: '',
|
||||
id: tvdbSeason.tvdbId,
|
||||
air_date: tvdbSeason.firstAired,
|
||||
season_number: episodes.length,
|
||||
};
|
||||
return await this.getTvdbSeasonData(tvdbId, seasonNumber, tvId, language);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[TVDB] Failed to fetch TV season details: ${error.message}`
|
||||
);
|
||||
return await this.tmdb.getTvSeason({ tvId, seasonNumber, language });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getSeasonIdentifier(req: any): number {
|
||||
public getSeasonIdentifier(req: any): number {
|
||||
return req.params.seasonId;
|
||||
}
|
||||
|
||||
private async enrichTmdbShowWithTvdbData(
|
||||
tmdbTvShow: TmdbTvDetails,
|
||||
tvdbId: ValidTvdbId,
|
||||
language: string
|
||||
): Promise<TmdbTvDetails> {
|
||||
try {
|
||||
const tvdbData = await this.fetchTvdbShowData(tvdbId, language);
|
||||
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,
|
||||
language: string
|
||||
): Promise<TvdbTvShowDetail> {
|
||||
return await this.get<TvdbTvShowDetail>(
|
||||
`/${language}/${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,
|
||||
language: string
|
||||
): Promise<TmdbSeasonWithEpisodes> {
|
||||
const tvdbSeason = await this.fetchTvdbShowData(tvdbId, language);
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user