Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
149 changes: 61 additions & 88 deletions src/controllers/deprecated/media.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import _ from 'lodash';
import episodeParser from 'episode-parser';

import { LookupFailedInternalError, MediaNotFoundError, ValidationError } from '../../helpers/customErrors';
import { MediaNotFoundError, ValidationError } from '../../helpers/customErrors';
import FailedLookups, { FailedLookupsInterface } from '../../models/FailedLookups';
import MediaMetadata, { MediaMetadataInterface } from '../../models/MediaMetadata';
import SeriesMetadata, { SeriesMetadataInterface } from '../../models/SeriesMetadata';
import * as externalAPIHelper from '../../services/external-api-helper';
import * as deprecatedExternalAPIHelper from '../../services/deprecated/external-api-helper';
import { addSearchMatchByIMDbID } from '../media';
import { traceLog } from '../../helpers/logging';

/**
* Since this is deprecated, it will only return a result that has been created
Expand Down Expand Up @@ -94,28 +93,76 @@ export const getByImdbID = async(ctx): Promise<MediaMetadataInterface | SeriesMe
};

/**
* @deprecated
* @deprecated since v11
*/
export const getSeries = async(ctx): Promise<SeriesMetadataInterface | MediaMetadataInterface> => {
const { imdbID, title, year }: UmsQueryParams = ctx.query;
if (!title && !imdbID) {
throw new ValidationError('Either IMDb ID or title required');
}

let dbMeta;
try {
const dbMeta = await externalAPIHelper.getSeriesMetadata(imdbID, title, null, year);
let searchMatch: string;
let parsedTitle: string;
if (title) {
// Extract the series name from the incoming string (usually not necessary)
const parsed = episodeParser(title);
parsedTitle = parsed?.show ? parsed.show : title;
searchMatch = parsedTitle;
}

let failedLookupQuery: FailedLookupsInterface;

if (imdbID) {
// We shouldn't have failures since we got this IMDb ID from their API
if (await FailedLookups.findOne({ imdbID }, '_id', { lean: true }).exec()) {
throw new MediaNotFoundError();
}

const existingSeries = await SeriesMetadata.findOne({ imdbID }, null, { lean: true }).exec();
if (existingSeries) {
dbMeta = existingSeries;
}
} else {
const sortBy = {};
const titleQuery: GetSeriesFilter = { searchMatches: { $in: [searchMatch] } };
failedLookupQuery = { title: parsedTitle, type: 'series' };

// language was not sent by UMS versions that used this endpoint (< v11)
failedLookupQuery.language = { $exists: false };

if (year) {
failedLookupQuery.startYear = year;
titleQuery.startYear = year;
} else {
sortBy['startYear'] = 1;
}

// Return early for previously-failed lookups
const previousFailedLookup = await FailedLookups.findOne(failedLookupQuery, '_id', { lean: true }).exec();
if (previousFailedLookup) {
const reason = `getSeriesMetadata found previous failed lookup ${JSON.stringify(failedLookupQuery)}`;
traceLog(reason);
throw new MediaNotFoundError();
}

// Return any previous match
traceLog('Looking for TV series in db', { parsedTitle });
const seriesMetadata = await SeriesMetadata.findOne(titleQuery, null, { lean: true }).sort(sortBy).exec();
if (seriesMetadata) {
traceLog('Found TV series', seriesMetadata);
dbMeta = seriesMetadata;
}
}

if (!dbMeta) {
throw new MediaNotFoundError();
}

const dbMetaWithPosters = await deprecatedExternalAPIHelper.addPosterFromImages(dbMeta);
return ctx.body = dbMetaWithPosters;
} catch (err) {
if (err instanceof LookupFailedInternalError) {
// in this case, the error came from getSeriesMetadata on the getSeries endpoint, and that already stores the reason, so there is nothing to do here but throw
throw new MediaNotFoundError();
}

// log unexpected errors
if (!(err instanceof MediaNotFoundError)) {
console.error(err);
Expand All @@ -125,25 +172,19 @@ export const getSeries = async(ctx): Promise<SeriesMetadataInterface | MediaMeta
};

/**
* @deprecated
* @deprecated since v11
*/
export const getVideo = async(ctx): Promise<MediaMetadataInterface> => {
const { title, imdbID }: UmsQueryParams = ctx.query;
const { episode, season, year }: UmsQueryParams = ctx.query;
const [seasonNumber, yearNumber] = [season, year].map(param => param ? Number(param) : null);
let episodeNumbers = null;
if (episode) {
const episodes = episode.split('-');
episodeNumbers = episodes.map(Number);
}

if (!title && !imdbID) {
throw new ValidationError('title or imdbId is a required parameter');
}

const query = [];
const failedQuery = [];
let imdbIdToSearch = imdbID;
const imdbIdToSearch = imdbID;

if (imdbIdToSearch) {
query.push({ imdbID: imdbIdToSearch });
Expand Down Expand Up @@ -176,73 +217,5 @@ export const getVideo = async(ctx): Promise<MediaMetadataInterface> => {
return ctx.body = existingResult;
}

const existingFailedResult = await FailedLookups.findOne({ $or: failedQuery }, null, { lean: true }).exec();
if (existingFailedResult) {
throw new MediaNotFoundError();
}

// the database does not have a record of this file, so begin search for metadata on TMDB.

const failedLookupQuery = { episode, imdbID, season, title, year };

if (!title && !imdbIdToSearch) {
// TMDB requires either a title or IMDb ID, so return if we don't have one
const reason = 'getVideo (deprecated) failed because there is no title or imdbId';
await FailedLookups.updateOne(failedLookupQuery, { $inc: { count: 1 }, reason }, { upsert: true, setDefaultsOnInsert: true }).exec();
throw new MediaNotFoundError();
}

// Start TMDB lookups
let tmdbData: MediaMetadataInterface;
try {
tmdbData = await externalAPIHelper.getFromTMDBAPI(title, null, imdbIdToSearch, yearNumber, seasonNumber, episodeNumbers);
imdbIdToSearch = imdbIdToSearch || tmdbData?.imdbID;
} catch (e) {
// Log the error but continue
if (e.message && e.message.includes('404') && e.response?.config?.url) {
console.log('Received 404 response from ' + e.response.config.url);
} else {
console.log(e);
}
}

// if the client did not pass an imdbID, but we found one on TMDB, see if we have an existing record for the now-known media.
if (!imdbID && imdbIdToSearch) {
{
const existingResult = await MediaMetadata.findOne({ imdbID: imdbIdToSearch }, null, { lean: true }).exec();
if (existingResult) {
return ctx.body = await addSearchMatchByIMDbID(imdbIdToSearch, title);
}
}
}
// End TMDB lookups

if (!tmdbData || _.isEmpty(tmdbData)) {
const reason = `getVideo (deprecated) failed because no data was found on TMDB for ${failedLookupQuery}`;
await FailedLookups.updateOne(failedLookupQuery, { $inc: { count: 1 }, reason }, { upsert: true, setDefaultsOnInsert: true }).exec();
throw new MediaNotFoundError();
}

try {
if (title) {
tmdbData.searchMatches = [title];
}

// Ensure that we return and cache the same episode number that was searched for
if (episodeNumbers && episodeNumbers.length > 1 && episodeNumbers[0] === tmdbData.episode) {
tmdbData.episode = episode;
}

const dbMeta = await MediaMetadata.create(tmdbData);

// TODO: Investigate why we need this "as" syntax
let leanMeta = dbMeta.toObject({ useProjection: true }) as MediaMetadataInterface;
leanMeta = await deprecatedExternalAPIHelper.addPosterFromImages(leanMeta);
return ctx.body = leanMeta;
} catch (e) {
console.error(e, tmdbData);
const reason = `getVideo (deprecated) failed because an error occurred: ${e}`;
await FailedLookups.updateOne(failedLookupQuery, { $inc: { count: 1 }, reason }, { upsert: true, setDefaultsOnInsert: true }).exec();
throw new MediaNotFoundError();
}
throw new MediaNotFoundError();
};
5 changes: 3 additions & 2 deletions src/controllers/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ export const addSearchMatchByIMDbID = async(imdbID: string, title: string): Prom
export const getLocalize = async(ctx): Promise<Partial<LocalizeMetadataInterface>> => {
const { language, mediaType, imdbID, tmdbID }: UmsQueryParams = ctx.query;
const { episode, season }: UmsQueryParams = ctx.query;
let seasonNumber: number|undefined;
let seasonNumber: number | undefined;
if (season) {
seasonNumber = Number(season);
}

if (!language || !mediaType || !(imdbID || tmdbID)) {
throw new ValidationError('Language, media type and either IMDb ID or TMDB ID are required');
const allQueryParams = { language, mediaType, imdbID, tmdbID, episode, season };
throw new ValidationError(`Language, media type and either IMDb ID or TMDB ID are required. Received ${JSON.stringify(allQueryParams)}`);
}
if (!language.match(/^[a-z]{2}(-[a-z]{2,4})?$/i)) {
throw new ValidationError(`Language must have a minimum length of 2 and follow the case-insensitive pattern: ([a-z]{2})-([a-z]{2,4}), received ${ctx.url}`);
Expand Down
2 changes: 1 addition & 1 deletion src/services/external-api-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export const getSeriesMetadata = async(
// Return early for previously-failed lookups
const previousFailedLookup = await FailedLookups.findOne(failedLookupQuery, '_id', { lean: true }).exec();
if (previousFailedLookup) {
const reason = `getSeriesMetadata found previous failed lookup ${failedLookupQuery.toString()}`;
const reason = `getSeriesMetadata found previous failed lookup ${JSON.stringify(failedLookupQuery)}`;
traceLog(reason);
await FailedLookups.updateOne(failedLookupQuery, { $inc: { count: 1 } }).exec();

Expand Down
Loading