mirror of
https://github.com/sct/overseerr.git
synced 2025-10-01 07:53:51 +02:00
feat: rotten tomatoes scores on movie/tv details pages
This commit is contained in:
150
server/api/rottentomatoes.ts
Normal file
150
server/api/rottentomatoes.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
interface RTMovieOldSearchResult {
|
||||
id: number;
|
||||
title: string;
|
||||
year: number;
|
||||
ratings: {
|
||||
critics_rating: 'Certified Fresh' | 'Fresh' | 'Rotten';
|
||||
critics_score: number;
|
||||
audience_rating: 'Upright' | 'Spilled';
|
||||
audience_score: number;
|
||||
};
|
||||
links: {
|
||||
self: string;
|
||||
alternate: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RTTvSearchResult {
|
||||
title: string;
|
||||
meterClass: 'fresh' | 'rotten';
|
||||
meterScore: number;
|
||||
url: string;
|
||||
startYear: number;
|
||||
endYear: number;
|
||||
}
|
||||
|
||||
interface RTMovieSearchResponse {
|
||||
total: number;
|
||||
movies: RTMovieOldSearchResult[];
|
||||
}
|
||||
|
||||
interface RTMultiSearchResponse {
|
||||
tvCount: number;
|
||||
tvSeries: RTTvSearchResult[];
|
||||
}
|
||||
|
||||
export interface RTRating {
|
||||
title: string;
|
||||
year: number;
|
||||
criticsRating: 'Certified Fresh' | 'Fresh' | 'Rotten';
|
||||
criticsScore: number;
|
||||
audienceRating?: 'Upright' | 'Spilled';
|
||||
audienceScore?: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a best-effort API. The Rotten Tomatoes API is technically
|
||||
* private and getting access costs money/requires approval.
|
||||
*
|
||||
* They do, however, have a "public" api that they use to request the
|
||||
* data on their own site. We use this to get ratings for movies/tv shows.
|
||||
*
|
||||
* Unfortunately, we need to do it by searching for the movie name, so it's
|
||||
* not always accurate.
|
||||
*/
|
||||
class RottenTomatoes {
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.axios = axios.create({
|
||||
baseURL: 'https://www.rottentomatoes.com/api/private',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the 1.0 api for the movie title
|
||||
*
|
||||
* We compare the release date to make sure its the correct
|
||||
* 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 year Release Year
|
||||
*/
|
||||
public async getMovieRatings(
|
||||
name: string,
|
||||
year: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const response = await this.axios.get<RTMovieSearchResponse>(
|
||||
'/v1.0/movies',
|
||||
{
|
||||
params: { q: name },
|
||||
}
|
||||
);
|
||||
|
||||
const movie = response.data.movies.find((movie) => movie.year === year);
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
title: movie.title,
|
||||
url: movie.links.alternate,
|
||||
criticsRating: movie.ratings.critics_rating,
|
||||
criticsScore: movie.ratings.critics_score,
|
||||
audienceRating: movie.ratings.audience_rating,
|
||||
audienceScore: movie.ratings.audience_score,
|
||||
year: movie.year,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[RT API] Failed to retrieve movie ratings: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTVRatings(
|
||||
name: string,
|
||||
year: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const response = await this.axios.get<RTMultiSearchResponse>(
|
||||
'/v2.0/search/',
|
||||
{
|
||||
params: { q: name, limit: 10 },
|
||||
}
|
||||
);
|
||||
|
||||
const tvshow = response.data.tvSeries.find(
|
||||
(series) => series.startYear === year
|
||||
);
|
||||
|
||||
if (!tvshow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
title: tvshow.title,
|
||||
url: `https://www.rottentomatoes.com${tvshow.url}`,
|
||||
criticsRating: tvshow.meterClass === 'fresh' ? 'Fresh' : 'Rotten',
|
||||
criticsScore: tvshow.meterScore,
|
||||
year: tvshow.startYear,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(`[RT API] Failed to retrieve tv ratings: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default RottenTomatoes;
|
@@ -4,6 +4,7 @@ import { mapMovieDetails } from '../models/Movie';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import { mapMovieResult } from '../models/Search';
|
||||
import Media from '../entity/Media';
|
||||
import RottenTomatoes from '../api/rottentomatoes';
|
||||
|
||||
const movieRoutes = Router();
|
||||
|
||||
@@ -72,4 +73,28 @@ movieRoutes.get('/:id/similar', async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
movieRoutes.get('/:id/ratings', async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
const rtapi = new RottenTomatoes();
|
||||
|
||||
const movie = await tmdb.getMovie({
|
||||
movieId: Number(req.params.id),
|
||||
});
|
||||
|
||||
if (!movie) {
|
||||
return next({ status: 404, message: 'Movie does not exist' });
|
||||
}
|
||||
|
||||
const rtratings = await rtapi.getMovieRatings(
|
||||
movie.title,
|
||||
Number(movie.release_date.slice(0, 4))
|
||||
);
|
||||
|
||||
if (!rtratings) {
|
||||
return next({ status: 404, message: 'Unable to retrieve ratings' });
|
||||
}
|
||||
|
||||
return res.status(200).json(rtratings);
|
||||
});
|
||||
|
||||
export default movieRoutes;
|
||||
|
@@ -4,6 +4,7 @@ import { MediaRequest } from '../entity/MediaRequest';
|
||||
import { mapTvDetails, mapSeasonWithEpisodes } from '../models/Tv';
|
||||
import { mapTvResult } from '../models/Search';
|
||||
import Media from '../entity/Media';
|
||||
import RottenTomatoes from '../api/rottentomatoes';
|
||||
|
||||
const tvRoutes = Router();
|
||||
|
||||
@@ -84,4 +85,28 @@ tvRoutes.get('/:id/similar', async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
tvRoutes.get('/:id/ratings', async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
const rtapi = new RottenTomatoes();
|
||||
|
||||
const tv = await tmdb.getTvShow({
|
||||
tvId: Number(req.params.id),
|
||||
});
|
||||
|
||||
if (!tv) {
|
||||
return next({ status: 404, message: 'TV Show does not exist' });
|
||||
}
|
||||
|
||||
const rtratings = await rtapi.getTVRatings(
|
||||
tv.name,
|
||||
Number(tv.first_air_date.slice(0, 4))
|
||||
);
|
||||
|
||||
if (!rtratings) {
|
||||
return next({ status: 404, message: 'Unable to retrieve ratings' });
|
||||
}
|
||||
|
||||
return res.status(200).json(rtratings);
|
||||
});
|
||||
|
||||
export default tvRoutes;
|
||||
|
Reference in New Issue
Block a user