Rotten Tomatoes info on IMDb

Fetches review count and average review score from Rotten Tomatoes

// ==UserScript==
// @name        Rotten Tomatoes info on IMDb
// @namespace   k226zzno1ibma5mvtmxc
// @version     1.2
// @description Fetches review count and average review score from Rotten Tomatoes
// @license     MIT
// @include     https://www.imdb.com/title/tt*
// @grant       GM.xmlHttpRequest
// @grant       GM.setValue
// @grant       GM.getValue
// @connect     www.omdbapi.com
// @connect     rottentomatoes.com
// @run-at      document-start
// ==/UserScript==

(async function () {
	"use strict";

	let apiKey = await GM.getValue("apiKey");

	if (!apiKey) {
		apiKey = prompt("Please enter your OMDb API key to use this userscript, or hit cancel if you need to sign up for one.");

		if (!apiKey) {
			if (apiKey === null) {
				location.href = "https://www.omdbapi.com/apikey.aspx";
			}
			return;
		} else {
			GM.setValue("apiKey", apiKey);
		}
	}

	apiKey = encodeURIComponent(apiKey);

	const id = location.pathname.match(/^\/title\/(tt\d{7})/)[1];

	// Cache data in milliseconds
	const cachePeriod = 3600000;
	const cachePrefix = `k226zzno1i-${id}-`;

	function cacheGet(key) {
		return sessionStorage[`${cachePrefix}${key}`];
	}

	function cacheSet(key, value) {
		sessionStorage[`${cachePrefix}${key}`] = value;
	}

	function isCacheCurrent(key) {
		const time = cacheGet(key);
		return time && (Date.now() - parseInt(time, 10)) < cachePeriod;
	}


	function queueTheTomato(url) {
		// Proceed if/when page is fully loaded
		if (document.readyState === "complete") {
			addTheTomato(url);
		} else {
			window.addEventListener("load", () => {
				addTheTomato(url);
			});
		}
	}

	function addTheTomato(url) {
		const link = document.createElement("a");
		link.target = "_blank";
		link.rel = "noopener";
		link.referrerPolicy = "no-referrer";
		link.title = "View on Rotten Tomatoes";
		link.href = url;
		link.append("\uD83C\uDF45"); // tomato emoji

		// New layout
		const nav = document.querySelector("[data-testid='hero-subnav-bar-topic-links']");
		if (!nav) {
			return;
		}

		const [template] = nav.getElementsByTagName("li");
		const [templateLink] = template.getElementsByTagName("a");

		link.className = templateLink.className;

		const newOption = template.cloneNode();
		newOption.append(link);
		nav.append(newOption);

		// Scrape ratings data from Rotten Tomatoes (or cache)
		if (isCacheCurrent("rtTime")) {
			link.append(` ${cacheGet("rtAverage")} (${cacheGet("rtNumReviews")})`);
		} else {
			GM.xmlHttpRequest({
				method: "GET",
				anonymous: true,
				url: url,
				onload: (response) => {
					const doc = new DOMParser().parseFromString(response.responseText, "text/html");
					// Scrape info from script tags
					const script = doc.getElementById("score-details-json");

					if (!script) {
						return;
					}

					// Parse as JSON, should probably always work
					let scoreInfo;
					try {
						scoreInfo = JSON.parse(script.textContent).modal.tomatometerScoreAll;
					} catch (ignore) {
						return;
					}


					// Cache data
					cacheSet("rtTime", Date.now());
					cacheSet("rtAverage",    scoreInfo.averageRating);
					cacheSet("rtNumReviews", scoreInfo.ratingCount);

					link.append(` ${scoreInfo.averageRating} (${scoreInfo.ratingCount})`);
				}
			});
		}
	}


	// Get Rotten Tomatoes movie alias from cache or OMDb API
	if (isCacheCurrent("omdbTime")) {
		queueTheTomato(cacheGet("rtURL"));
	} else {
		GM.xmlHttpRequest({
			method: "GET",
			anonymous: true,
			url: `https://www.omdbapi.com/?apikey=${apiKey}&tomatoes=true&i=${encodeURIComponent(id)}`,
			onload: (response) => {
				const json = JSON.parse(response.responseText);
				if (json && json.tomatoURL) {
					let url;
					try {
						// Check if URL is valid
						url = new URL(json.tomatoURL);
					} catch (ignore) {
						return;
					}

					// Cache data
					cacheSet("rtURL",    url.href);
					cacheSet("omdbTime", Date.now());

					queueTheTomato(url.href);
				} else if (json && json.Error) {
					console.log(`Error:  ${json.Error}`);
				}
			}
		});
	}
})();