mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
fix(experimental): use new RT API (sorta) (#3179)
This commit is contained in:
@@ -69,6 +69,30 @@ class ExternalAPI {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async post<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
ttl?: number
|
||||||
|
): Promise<T> {
|
||||||
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
|
config: config?.params,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||||
|
if (cachedItem) {
|
||||||
|
return cachedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.axios.post<T>(endpoint, data, config);
|
||||||
|
|
||||||
|
if (this.cache) {
|
||||||
|
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
protected async getRolling<T>(
|
protected async getRolling<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
config?: AxiosRequestConfig,
|
config?: AxiosRequestConfig,
|
||||||
|
@@ -1,28 +1,40 @@
|
|||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
import ExternalAPI from './externalapi';
|
import ExternalAPI from './externalapi';
|
||||||
|
|
||||||
interface RTSearchResult {
|
interface RTAlgoliaSearchResponse {
|
||||||
meterClass: 'certified_fresh' | 'fresh' | 'rotten';
|
results: {
|
||||||
meterScore: number;
|
hits: RTAlgoliaHit[];
|
||||||
url: string;
|
index: 'content_rt' | 'people_rt';
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RTTvSearchResult extends RTSearchResult {
|
interface RTAlgoliaHit {
|
||||||
|
emsId: string;
|
||||||
|
emsVersionId: string;
|
||||||
|
tmsId: string;
|
||||||
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
startYear: number;
|
titles: string[];
|
||||||
endYear: number;
|
description: string;
|
||||||
}
|
releaseYear: string;
|
||||||
interface RTMovieSearchResult extends RTSearchResult {
|
rating: string;
|
||||||
name: string;
|
genres: string[];
|
||||||
url: string;
|
updateDate: string;
|
||||||
year: number;
|
isEmsSearchable: boolean;
|
||||||
}
|
rtId: number;
|
||||||
|
vanity: string;
|
||||||
interface RTMultiSearchResponse {
|
aka: string[];
|
||||||
tvCount: number;
|
posterImageUrl: string;
|
||||||
tvSeries: RTTvSearchResult[];
|
rottenTomatoes: {
|
||||||
movieCount: number;
|
audienceScore: number;
|
||||||
movies: RTMovieSearchResult[];
|
criticsIconUrl: string;
|
||||||
|
wantToSeeCount: number;
|
||||||
|
audienceIconUrl: string;
|
||||||
|
scoreSentiment: string;
|
||||||
|
certifiedFresh: boolean;
|
||||||
|
criticsScore: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RTRating {
|
export interface RTRating {
|
||||||
@@ -47,13 +59,20 @@ export interface RTRating {
|
|||||||
*/
|
*/
|
||||||
class RottenTomatoes extends ExternalAPI {
|
class RottenTomatoes extends ExternalAPI {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
const settings = getSettings();
|
||||||
super(
|
super(
|
||||||
'https://www.rottentomatoes.com/api/private',
|
'https://79frdp12pn-dsn.algolia.net/1/indexes/*',
|
||||||
{},
|
{
|
||||||
|
'x-algolia-agent':
|
||||||
|
'Algolia%20for%20JavaScript%20(4.14.3)%3B%20Browser%20(lite)',
|
||||||
|
'x-algolia-api-key': '175588f6e5f8319b27702e4cc4013561',
|
||||||
|
'x-algolia-application-id': '79FRDP12PN',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
'x-algolia-usertoken': settings.clientId,
|
||||||
},
|
},
|
||||||
nodeCache: cacheManager.getCache('rt').data,
|
nodeCache: cacheManager.getCache('rt').data,
|
||||||
}
|
}
|
||||||
@@ -61,14 +80,11 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search the 1.0 api for the movie title
|
* Search the RT algolia api for the movie title
|
||||||
*
|
*
|
||||||
* We compare the release date to make sure its the correct
|
* We compare the release date to make sure its the correct
|
||||||
* match. But it's not guaranteed to have results.
|
* match. But it's not guaranteed to have results.
|
||||||
*
|
*
|
||||||
* We use the 1.0 API here because the 2.0 search api does
|
|
||||||
* not return audience ratings.
|
|
||||||
*
|
|
||||||
* @param name Movie name
|
* @param name Movie name
|
||||||
* @param year Release Year
|
* @param year Release Year
|
||||||
*/
|
*/
|
||||||
@@ -77,30 +93,45 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
year: number
|
year: number
|
||||||
): Promise<RTRating | null> {
|
): Promise<RTRating | null> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
||||||
params: { q: name, limit: 10 },
|
requests: [
|
||||||
|
{
|
||||||
|
indexName: 'content_rt',
|
||||||
|
query: name,
|
||||||
|
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
||||||
|
|
||||||
|
if (!contentResults) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// First, attempt to match exact name and year
|
// First, attempt to match exact name and year
|
||||||
let movie = data.movies.find(
|
let movie = contentResults.hits.find(
|
||||||
(movie) => movie.year === year && movie.name === name
|
(movie) => movie.releaseYear === year.toString() && movie.title === name
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we don't find a movie, try to match partial name and year
|
// If we don't find a movie, try to match partial name and year
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
movie = data.movies.find(
|
movie = contentResults.hits.find(
|
||||||
(movie) => movie.year === year && movie.name.includes(name)
|
(movie) =>
|
||||||
|
movie.releaseYear === year.toString() && movie.title.includes(name)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we still dont find a movie, try to match just on year
|
// If we still dont find a movie, try to match just on year
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
movie = data.movies.find((movie) => movie.year === year);
|
movie = contentResults.hits.find(
|
||||||
|
(movie) => movie.releaseYear === year.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One last try, try exact name match only
|
// One last try, try exact name match only
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
movie = data.movies.find((movie) => movie.name === name);
|
movie = contentResults.hits.find((movie) => movie.title === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
@@ -108,16 +139,15 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: movie.name,
|
title: movie.title,
|
||||||
url: `https://www.rottentomatoes.com${movie.url}`,
|
url: `https://www.rottentomatoes.com/m/${movie.vanity}`,
|
||||||
criticsRating:
|
criticsRating: movie.rottenTomatoes.certifiedFresh
|
||||||
movie.meterClass === 'certified_fresh'
|
? 'Certified Fresh'
|
||||||
? 'Certified Fresh'
|
: movie.rottenTomatoes.criticsScore >= 60
|
||||||
: movie.meterClass === 'fresh'
|
? 'Fresh'
|
||||||
? 'Fresh'
|
: 'Rotten',
|
||||||
: 'Rotten',
|
criticsScore: movie.rottenTomatoes.criticsScore,
|
||||||
criticsScore: movie.meterScore,
|
year: Number(movie.releaseYear),
|
||||||
year: movie.year,
|
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -131,14 +161,28 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
year?: number
|
year?: number
|
||||||
): Promise<RTRating | null> {
|
): Promise<RTRating | null> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
||||||
params: { q: name, limit: 10 },
|
requests: [
|
||||||
|
{
|
||||||
|
indexName: 'content_rt',
|
||||||
|
query: name,
|
||||||
|
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
let tvshow: RTTvSearchResult | undefined = data.tvSeries[0];
|
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
||||||
|
|
||||||
|
if (!contentResults) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tvshow: RTAlgoliaHit | undefined = contentResults.hits[0];
|
||||||
|
|
||||||
if (year) {
|
if (year) {
|
||||||
tvshow = data.tvSeries.find((series) => series.startYear === year);
|
tvshow = contentResults.hits.find(
|
||||||
|
(series) => series.releaseYear === year.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tvshow) {
|
if (!tvshow) {
|
||||||
@@ -147,10 +191,11 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
title: tvshow.title,
|
title: tvshow.title,
|
||||||
url: `https://www.rottentomatoes.com${tvshow.url}`,
|
url: `https://www.rottentomatoes.com/tv/${tvshow.vanity}`,
|
||||||
criticsRating: tvshow.meterClass === 'fresh' ? 'Fresh' : 'Rotten',
|
criticsRating:
|
||||||
criticsScore: tvshow.meterScore,
|
tvshow.rottenTomatoes.criticsScore >= 60 ? 'Fresh' : 'Rotten',
|
||||||
year: tvshow.startYear,
|
criticsScore: tvshow.rottenTomatoes.criticsScore,
|
||||||
|
year: Number(tvshow.releaseYear),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[RT API] Failed to retrieve tv ratings: ${e.message}`);
|
throw new Error(`[RT API] Failed to retrieve tv ratings: ${e.message}`);
|
||||||
|
Reference in New Issue
Block a user