diff --git a/scripts/match/find-rotten-tomatoes-match.js b/scripts/match/find-rotten-tomatoes-match.js index 33afc2fb5..731bb6f16 100644 --- a/scripts/match/find-rotten-tomatoes-match.js +++ b/scripts/match/find-rotten-tomatoes-match.js @@ -87,6 +87,19 @@ 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 ? { @@ -94,18 +107,17 @@ const getScoresFor = (group) => 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), @@ -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, @@ -127,3 +142,4 @@ async function findRottenTomatoesMatch(movie) { } module.exports = findRottenTomatoesMatch; +module.exports.parseMoviePage = parseMoviePage; diff --git a/scripts/match/tests/find-rotten-tomatoes-match.test.js b/scripts/match/tests/find-rotten-tomatoes-match.test.js new file mode 100644 index 000000000..40b0ad6ee --- /dev/null +++ b/scripts/match/tests/find-rotten-tomatoes-match.test.js @@ -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"', + ); + }); +}); diff --git a/scripts/match/tests/fixtures/rotten-tomatoes-harakiri.html b/scripts/match/tests/fixtures/rotten-tomatoes-harakiri.html new file mode 100644 index 000000000..5f2e11f3b --- /dev/null +++ b/scripts/match/tests/fixtures/rotten-tomatoes-harakiri.html @@ -0,0 +1,62 @@ + + + +
+