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
26 changes: 21 additions & 5 deletions scripts/match/find-rotten-tomatoes-match.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,25 +87,37 @@ const getRottenTomatoesMatch = async (movie) => {
return getMatch(movie, getSearchResults, getDirectorsForMatch);
};

// The scorecard gives the Tomatometer and Popcornmeter as `scorePercent`
// ("100%"). It used to be a bare `score`, and when RT renamed it in November
// 2026 every film's score read as missing without anything failing, so a value
// that is present but not a percentage throws rather than parsing to NaN.
const parseScorePercent = (scorePercent) => {
if (scorePercent === undefined) return undefined;
const match = /^(\d{1,3})%$/.exec(scorePercent);
if (!match) {
throw new Error(`Unrecognised Rotten Tomatoes score "${scorePercent}"`);
}
return parseInt(match[1], 10);
};

const getScoresFor = (group) =>
group
? {
likes: group.likedCount,
dislikes: group.notLikedCount,
reviews: group.reviewCount,
rating: parseFloat(group.averageRating),
score: parseInt(group.score, 10),
score: parseScorePercent(group.scorePercent),
}
: undefined;

const getScore = async (match) => {
const rottenTomatoesGet = await getMoviePage(match);
const $ = cheerio.load(rottenTomatoesGet);
const parseMoviePage = (moviePage, url) => {
const $ = cheerio.load(moviePage);
const id = $("watchlist-button").attr("ems-id");
const scorecard = JSON.parse(getText($("#media-scorecard-json")));
return {
id,
url: match.url,
url,
audience: {
all: getScoresFor(scorecard.overlay.audienceAll),
verified: getScoresFor(scorecard.overlay.audienceVerified),
Expand All @@ -117,6 +129,9 @@ const getScore = async (match) => {
};
};

const getScore = async (match) =>
parseMoviePage(await getMoviePage(match), match.url);

async function findRottenTomatoesMatch(movie) {
return findSourceMatch(movie, {
getMatch: getRottenTomatoesMatch,
Expand All @@ -127,3 +142,4 @@ async function findRottenTomatoesMatch(movie) {
}

module.exports = findRottenTomatoesMatch;
module.exports.parseMoviePage = parseMoviePage;
55 changes: 55 additions & 0 deletions scripts/match/tests/find-rotten-tomatoes-match.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const fs = require("node:fs");
const path = require("node:path");
const { parseMoviePage } = require("../find-rotten-tomatoes-match");

const harakiri = fs.readFileSync(
path.join(__dirname, "fixtures", "rotten-tomatoes-harakiri.html"),
"utf8",
);
const URL = "https://www.rottentomatoes.com/m/harakiri";

describe("parseMoviePage", () => {
it("reads the Tomatometer and Popcornmeter from the scorecard", () => {
expect(parseMoviePage(harakiri, URL)).toEqual({
id: "b140bf70-abc3-3c32-8c7b-23495a8030f9",
url: URL,
audience: {
all: {
likes: undefined,
dislikes: undefined,
reviews: undefined,
rating: 4.6,
score: 96,
},
// No verified ratings: the group is there, with nothing in it
verified: {
likes: undefined,
dislikes: undefined,
reviews: undefined,
rating: NaN,
score: undefined,
},
},
critics: {
all: { likes: 10, dislikes: 0, reviews: 10, rating: 8.6, score: 100 },
top: {
likes: 3,
dislikes: 0,
reviews: 3,
rating: NaN,
score: undefined,
},
},
});
});

it("throws on a score that isn't a percentage", () => {
const page = harakiri.replace(
'"scorePercent": "100%"',
'"scorePercent": "100"',
);
expect(() => parseMoviePage(page, URL)).toThrow(
'Unrecognised Rotten Tomatoes score "100"',
);
});
});
62 changes: 62 additions & 0 deletions scripts/match/tests/fixtures/rotten-tomatoes-harakiri.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!doctype html>
<!-- Trimmed from https://www.rottentomatoes.com/m/harakiri, fetched 2026-11-11 -->
<html>
<body>
<watchlist-button slot="watchlist-cta" ems-id="b140bf70-abc3-3c32-8c7b-23495a8030f9" media-type="Movie" media-title="Hara-Kiri" state="unchecked" theme="transparent-lighttext" data-heromodulesmanager="mediaHeroWatchlistBtn" data-watchlistbuttonmanager="watchlistButton:click"></watchlist-button>
<script id="media-scorecard-json" data-json="props" type="application/json">
{
"overlay": {
"audienceAll": {
"averageRating": "4.6",
"bandedRatingCount": "5,000+ Ratings",
"certified": false,
"reviewsPageUrl": "/m/harakiri/reviews/all-audience",
"scorePercent": "96%",
"sentiment": "POSITIVE",
"title": "Popcornmeter",
"scoreLinkUrl": "/m/harakiri/reviews/all-audience"
},
"audienceTitle": "Popcornmeter",
"audienceVerified": {
"bandedRatingCount": "0 Verified Ratings",
"certified": false,
"title": "Popcornmeter",
"scoreLinkUrl": "/m/harakiri/reviews/verified-audience"
},
"criticsAll": {
"averageRating": "8.60",
"certified": false,
"likedCount": 10,
"notLikedCount": 0,
"ratingCount": 10,
"reviewCount": 10,
"reviewsPageUrl": "/m/harakiri/reviews",
"scorePercent": "100%",
"sentiment": "POSITIVE",
"title": "Tomatometer",
"scoreLinkUrl": "/m/harakiri/reviews/all-critics",
"scoreLinkText": "10 Reviews"
},
"criticsTitle": "Tomatometer",
"criticsTop": {
"likedCount": 3,
"notLikedCount": 0,
"ratingCount": 3,
"reviewCount": 3,
"reviewsPageUrl": "/m/harakiri/reviews",
"title": "Tomatometer",
"scoreLinkUrl": "/m/harakiri/reviews/top-critics",
"scoreLinkText": "3 Top Critic Reviews"
},
"hasAudienceAll": true,
"hasAudienceVerified": false,
"hasCriticsAll": true,
"hasCriticsTop": false,
"mediaType": "Movie",
"showScoreDetailsAudience": true,
"learnMoreUrl": "https://editorial.rottentomatoes.com/article/introducing-verified-audience-score/"
}
}
</script>
</body>
</html>
Loading