Skip to content

Commit

Permalink
feat(rating): added IMDB Radarr proxy (#3496)
Browse files Browse the repository at this point in the history
* feat(rating): added imdb radarr proxy

Signed-off-by: marcofaggian <m@marcofaggian.com>

* refactor(rating/imdb): rm export unused interfaces

Signed-off-by: marcofaggian <m@marcofaggian.com>

* docs(rating/imdb): rt to imdb

Signed-off-by: marcofaggian <m@marcofaggian.com>

* refactor(rating/imdb): specified error message

Signed-off-by: marcofaggian <m@marcofaggian.com>

* refactor(rating/imdb): rm line break

Signed-off-by: marcofaggian <m@marcofaggian.com>

* refactor(rating): conform to types patter

Signed-off-by: marcofaggian <m@marcofaggian.com>

* chore(rating/imdb): added line to translation file

Signed-off-by: marcofaggian <m@marcofaggian.com>

* feat(rating/imdb): ratings to ratingscombined

Signed-off-by: marcofaggian <m@marcofaggian.com>

* fix(rating/imdb): reinstating ratings route

Signed-off-by: marcofaggian <m@marcofaggian.com>

* docs(ratings): openapi ratings

Signed-off-by: marcofaggian <m@marcofaggian.com>

* chore(ratings): undo openapi ratings apex

Signed-off-by: marcofaggian <m@marcofaggian.com>

---------

Signed-off-by: marcofaggian <m@marcofaggian.com>
  • Loading branch information
marcofaggian committed Jul 28, 2023
1 parent 83b008c commit b4191f9
Show file tree
Hide file tree
Showing 10 changed files with 384 additions and 39 deletions.
57 changes: 57 additions & 0 deletions overseerr-api.yml
Expand Up @@ -5338,6 +5338,63 @@ paths:
audienceRating:
type: string
enum: ['Spilled', 'Upright']
/movie/{movieId}/ratingscombined:
get:
summary: Get RT and IMDB movie ratings combined
description: Returns ratings from RottenTomatoes and IMDB based on the provided movieId in a JSON object.
tags:
- movies
parameters:
- in: path
name: movieId
required: true
schema:
type: number
example: 337401
responses:
'200':
description: Ratings returned
content:
application/json:
schema:
type: object
properties:
rt:
type: object
properties:
title:
type: string
example: Mulan
year:
type: number
example: 2020
url:
type: string
example: 'http://www.rottentomatoes.com/m/mulan_2020/'
criticsScore:
type: number
example: 85
criticsRating:
type: string
enum: ['Rotten', 'Fresh', 'Certified Fresh']
audienceScore:
type: number
example: 65
audienceRating:
type: string
enum: ['Spilled', 'Upright']
imdb:
type: object
properties:
title:
type: string
example: I am Legend
url:
type: string
example: 'https://www.imdb.com/title/tt0480249'
criticsScore:
type: number
example: 6.5
/tv/{tvId}:
get:
summary: Get TV details
Expand Down
195 changes: 195 additions & 0 deletions server/api/rating/imdbRadarrProxy.ts
@@ -0,0 +1,195 @@
import ExternalAPI from '@server/api/externalapi';
import cacheManager from '@server/lib/cache';

type IMDBRadarrProxyResponse = IMDBMovie[];

interface IMDBMovie {
ImdbId: string;
Overview: string;
Title: string;
OriginalTitle: string;
TitleSlug: string;
Ratings: Rating[];
MovieRatings: MovieRatings;
Runtime: number;
Images: Image[];
Genres: string[];
Popularity: number;
Premier: string;
InCinema: string;
PhysicalRelease: any;
DigitalRelease: string;
Year: number;
AlternativeTitles: AlternativeTitle[];
Translations: Translation[];
Recommendations: Recommendation[];
Credits: Credits;
Studio: string;
YoutubeTrailerId: string;
Certifications: Certification[];
Status: any;
Collection: Collection;
OriginalLanguage: string;
Homepage: string;
TmdbId: number;
}

interface Rating {
Count: number;
Value: number;
Origin: string;
Type: string;
}

interface MovieRatings {
Tmdb: Tmdb;
Imdb: Imdb;
Metacritic: Metacritic;
RottenTomatoes: RottenTomatoes;
}

interface Tmdb {
Count: number;
Value: number;
Type: string;
}

interface Imdb {
Count: number;
Value: number;
Type: string;
}

interface Metacritic {
Count: number;
Value: number;
Type: string;
}

interface RottenTomatoes {
Count: number;
Value: number;
Type: string;
}

interface Image {
CoverType: string;
Url: string;
}

interface AlternativeTitle {
Title: string;
Type: string;
Language: string;
}

interface Translation {
Title: string;
Overview: string;
Language: string;
}

interface Recommendation {
TmdbId: number;
Title: string;
}

interface Credits {
Cast: Cast[];
Crew: Crew[];
}

interface Cast {
Name: string;
Order: number;
Character: string;
TmdbId: number;
CreditId: string;
Images: Image2[];
}

interface Image2 {
CoverType: string;
Url: string;
}

interface Crew {
Name: string;
Job: string;
Department: string;
TmdbId: number;
CreditId: string;
Images: Image3[];
}

interface Image3 {
CoverType: string;
Url: string;
}

interface Certification {
Country: string;
Certification: string;
}

interface Collection {
Name: string;
Images: any;
Overview: any;
Translations: any;
Parts: any;
TmdbId: number;
}

export interface IMDBRating {
title: string;
url: string;
criticsScore: number;
}

/**
* This is a best-effort API. The IMDB API is technically
* private and getting access costs money/requires approval.
*
* Radarr hosts a public proxy that's in use by all Radarr instances.
*/
class IMDBRadarrProxy extends ExternalAPI {
constructor() {
super('https://api.radarr.video/v1', {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
nodeCache: cacheManager.getCache('imdb').data,
});
}

/**
* Ask the Radarr IMDB Proxy for the movie
*
* @param IMDBid Id of IMDB movie
*/
public async getMovieRatings(IMDBid: string): Promise<IMDBRating | null> {
try {
const data = await this.get<IMDBRadarrProxyResponse>(
`/movie/imdb/${IMDBid}`
);

if (!data?.length || data[0].ImdbId !== IMDBid) {
return null;
}

return {
title: data[0].Title,
url: `https://www.imdb.com/title/${data[0].ImdbId}`,
criticsScore: data[0].MovieRatings.Imdb.Value,
};
} catch (e) {
throw new Error(
`[IMDB RADARR PROXY API] Failed to retrieve movie ratings: ${e.message}`
);
}
}
}

export default IMDBRadarrProxy;
@@ -1,6 +1,6 @@
import ExternalAPI from '@server/api/externalapi';
import cacheManager from '@server/lib/cache';
import { getSettings } from '@server/lib/settings';
import ExternalAPI from './externalapi';

interface RTAlgoliaSearchResponse {
results: {
Expand Down Expand Up @@ -144,6 +144,9 @@ class RottenTomatoes extends ExternalAPI {
? 'Fresh'
: 'Rotten',
criticsScore: movie.rottenTomatoes.criticsScore,
audienceRating:
movie.rottenTomatoes.audienceScore >= 60 ? 'Upright' : 'Spilled',
audienceScore: movie.rottenTomatoes.audienceScore,
year: Number(movie.releaseYear),
};
} catch (e) {
Expand Down Expand Up @@ -192,6 +195,9 @@ class RottenTomatoes extends ExternalAPI {
criticsRating:
tvshow.rottenTomatoes.criticsScore >= 60 ? 'Fresh' : 'Rotten',
criticsScore: tvshow.rottenTomatoes.criticsScore,
audienceRating:
tvshow.rottenTomatoes.audienceScore >= 60 ? 'Upright' : 'Spilled',
audienceScore: tvshow.rottenTomatoes.audienceScore,
year: Number(tvshow.releaseYear),
};
} catch (e) {
Expand Down
7 changes: 7 additions & 0 deletions server/api/ratings.ts
@@ -0,0 +1,7 @@
import { type IMDBRating } from '@server/api/rating/imdbRadarrProxy';
import { type RTRating } from '@server/api/rating/rottentomatoes';

export interface RatingResponse {
rt?: RTRating;
imdb?: IMDBRating;
}
5 changes: 5 additions & 0 deletions server/lib/cache.ts
Expand Up @@ -5,6 +5,7 @@ export type AvailableCacheIds =
| 'radarr'
| 'sonarr'
| 'rt'
| 'imdb'
| 'github'
| 'plexguid'
| 'plextv';
Expand Down Expand Up @@ -51,6 +52,10 @@ class CacheManager {
stdTtl: 43200,
checkPeriod: 60 * 30,
}),
imdb: new Cache('imdb', 'IMDB Radarr Proxy', {
stdTtl: 43200,
checkPeriod: 60 * 30,
}),
github: new Cache('github', 'GitHub API', {
stdTtl: 21600,
checkPeriod: 60 * 30,
Expand Down
56 changes: 55 additions & 1 deletion server/routes/movie.ts
@@ -1,4 +1,6 @@
import RottenTomatoes from '@server/api/rottentomatoes';
import IMDBRadarrProxy from '@server/api/rating/imdbRadarrProxy';
import RottenTomatoes from '@server/api/rating/rottentomatoes';
import { type RatingResponse } from '@server/api/ratings';
import TheMovieDb from '@server/api/themoviedb';
import { MediaType } from '@server/constants/media';
import Media from '@server/entity/Media';
Expand Down Expand Up @@ -116,6 +118,9 @@ movieRoutes.get('/:id/similar', async (req, res, next) => {
}
});

/**
* Endpoint backed by RottenTomatoes
*/
movieRoutes.get('/:id/ratings', async (req, res, next) => {
const tmdb = new TheMovieDb();
const rtapi = new RottenTomatoes();
Expand Down Expand Up @@ -151,4 +156,53 @@ movieRoutes.get('/:id/ratings', async (req, res, next) => {
}
});

/**
* Endpoint combining RottenTomatoes and IMDB
*/
movieRoutes.get('/:id/ratingscombined', async (req, res, next) => {
const tmdb = new TheMovieDb();
const rtapi = new RottenTomatoes();
const imdbApi = new IMDBRadarrProxy();

try {
const movie = await tmdb.getMovie({
movieId: Number(req.params.id),
});

const rtratings = await rtapi.getMovieRatings(
movie.title,
Number(movie.release_date.slice(0, 4))
);

let imdbRatings;
if (movie.imdb_id) {
imdbRatings = await imdbApi.getMovieRatings(movie.imdb_id);
}

if (!rtratings && !imdbRatings) {
return next({
status: 404,
message: 'No ratings found.',
});
}

const ratings: RatingResponse = {
...(rtratings ? { rt: rtratings } : {}),
...(imdbRatings ? { imdb: imdbRatings } : {}),
};

return res.status(200).json(ratings);
} catch (e) {
logger.debug('Something went wrong retrieving movie ratings', {
label: 'API',
errorMessage: e.message,
movieId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve movie ratings.',
});
}
});

export default movieRoutes;
2 changes: 1 addition & 1 deletion server/routes/tv.ts
@@ -1,4 +1,4 @@
import RottenTomatoes from '@server/api/rottentomatoes';
import RottenTomatoes from '@server/api/rating/rottentomatoes';
import TheMovieDb from '@server/api/themoviedb';
import { MediaType } from '@server/constants/media';
import Media from '@server/entity/Media';
Expand Down

0 comments on commit b4191f9

Please sign in to comment.