refactor: clean tvdb indexer code

This commit is contained in:
TOomaAh
2024-10-22 22:57:54 +02:00
parent 7107f1e91f
commit 1fbe4d2031

View File

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