- // ==UserScript==
- // @name Show Rottentomatoes meter
- // @description Show Rotten Tomatoes score on imdb.com, metacritic.com, letterboxd.com, BoxOfficeMojo, serienjunkies.de, Amazon, Google Play, allmovie.com, Wikipedia, themoviedb.org, movies.com, tvmaze.com, tvguide.com, followshows.com, thetvdb.com, tvnfo.com, save.tv
- // @namespace cuzi
- // @grant GM_xmlhttpRequest
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant unsafeWindow
- // @grant GM.xmlHttpRequest
- // @grant GM.setValue
- // @grant GM.getValue
- // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js
- // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
- // @icon https://raw.githubusercontent.com/hfg-gmuend/openmoji/master/color/72x72/1F345.png
- // @version 48
- // @connect www.rottentomatoes.com
- // @connect algolia.net
- // @connect flixster.com
- // @connect imdb.com
- // @match https://www.rottentomatoes.com/
- // @match https://play.google.com/store/movies/details/*
- // @match https://www.amazon.ca/*
- // @match https://www.amazon.co.jp/*
- // @match https://www.amazon.co.uk/*
- // @match https://smile.amazon.co.uk/*
- // @match https://www.amazon.com.au/*
- // @match https://www.amazon.com.mx/*
- // @match https://www.amazon.com/*
- // @match https://smile.amazon.com/*
- // @match https://www.amazon.de/*
- // @match https://smile.amazon.de/*
- // @match https://www.amazon.es/*
- // @match https://www.amazon.fr/*
- // @match https://www.amazon.in/*
- // @match https://www.amazon.it/*
- // @match https://www.imdb.com/title/*
- // @match https://www.serienjunkies.de/*
- // @match http://www.serienjunkies.de/*
- // @match https://www.boxofficemojo.com/movies/*
- // @match https://www.boxofficemojo.com/release/*
- // @match https://www.allmovie.com/movie/*
- // @match https://en.wikipedia.org/*
- // @match https://www.fandango.com/*
- // @match https://www.themoviedb.org/movie/*
- // @match https://www.themoviedb.org/tv/*
- // @match https://letterboxd.com/film/*
- // @match https://letterboxd.com/film/*/image*
- // @match https://www.tvmaze.com/shows/*
- // @match https://www.tvguide.com/tvshows/*
- // @match https://followshows.com/show/*
- // @match https://thetvdb.com/series/*
- // @match https://thetvdb.com/movies/*
- // @match https://tvnfo.com/tv/*
- // @match https://www.metacritic.com/movie/*
- // @match https://www.metacritic.com/tv/*
- // @match https://www.nme.com/reviews/*
- // @match https://itunes.apple.com/*
- // @match https://epguides.com/*
- // @match https://www.epguides.com/*
- // @match https://www.cc.com/*
- // @match https://www.amc.com/*
- // @match https://www.amcplus.com/*
- // @match https://rlsbb.ru/*/
- // @match https://www.sho.com/*
- // @match https://www.gog.com/*
- // @match https://psa.wf/*
- // @match https://www.save.tv/*
- // @match https://www.wikiwand.com/*
- // @match https://trakt.tv/*
- // ==/UserScript==
-
- /* global GM, $, unsafeWindow */
- /* jshint asi: true, esversion: 8 */
-
- const scriptName = 'Show Rottentomatoes meter'
- const baseURL = 'https://www.rottentomatoes.com'
- const baseURLOpenTab = baseURL + '/search/?search={query}'
- const algoliaURL = 'https://{domain}-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent={agent}&x-algolia-api-key={sId}&x-algolia-application-id={aId}'
- const algoliaAgent = 'Algolia for JavaScript (4.12.0); Browser (lite)'
- const flixsterEMSURL = 'https://flixster.com/api/ems/v2/emsId/{emsId}'
- const cacheExpireAfterHours = 4
- const emojiTomato = String.fromCodePoint(0x1F345)
- const emojiGreenApple = String.fromCodePoint(0x1F34F)
- const emojiStrawberry = String.fromCodePoint(0x1F353)
-
- const emojiPopcorn = '\uD83C\uDF7F'
- const emojiGreenSalad = '\uD83E\uDD57'
- const emojiNauseated = '\uD83E\uDD22'
-
- // Detect dark theme of darkreader.org extension or normal css dark theme from browser
- const darkTheme = ('darkreaderScheme' in document.documentElement.dataset && document.documentElement.dataset.darkreaderScheme) || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
-
- function minutesSince (time) {
- const seconds = ((new Date()).getTime() - time.getTime()) / 1000
- return seconds > 60 ? parseInt(seconds / 60) + ' min ago' : 'now'
- }
-
- function intersection (setA, setB) {
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
- const _intersection = new Set()
- for (const elem of setB) {
- if (setA.has(elem)) {
- _intersection.add(elem)
- }
- }
- return _intersection
- }
-
- function asyncRequest (data) { // No cache (unlike in the Metacritic userscript)
- return new Promise(function (resolve, reject) {
- const defaultHeaders = {
- Referer: data.url,
- 'User-Agent': navigator.userAgent
- }
- const defaultData = {
- method: 'GET',
- onload: (response) => resolve(response),
- onerror: (response) => reject(response)
- }
- if ('headers' in data) {
- data.headers = Object.assign(defaultHeaders, data.headers)
- } else {
- data.headers = defaultHeaders
- }
- data = Object.assign(defaultData, data)
- console.debug(`${scriptName}: GM.xmlHttpRequest`, data)
- GM.xmlHttpRequest(data)
- })
- }
-
- const parseLDJSONCache = {}
- function parseLDJSON (keys, condition) {
- if (document.querySelector('script[type="application/ld+json"]')) {
- const xmlEntitiesElement = document.createElement('div')
- const xmlEntitiesPattern = /&(?:#x[a-f0-9]+|#[0-9]+|[a-z0-9]+);?/ig
- const xmlEntities = function (s) {
- s = s.replace(xmlEntitiesPattern, (m) => {
- xmlEntitiesElement.innerHTML = m
- return xmlEntitiesElement.textContent
- })
- return s
- }
- const decodeXmlEntities = function (jsonObj) {
- // Traverse through object, decoding all strings
- if (jsonObj !== null && typeof jsonObj === 'object') {
- Object.entries(jsonObj).forEach(([key, value]) => {
- // key is either an array index or object key
- jsonObj[key] = decodeXmlEntities(value)
- })
- } else if (typeof jsonObj === 'string') {
- return xmlEntities(jsonObj)
- }
- return jsonObj
- }
-
- const data = []
- const scripts = document.querySelectorAll('script[type="application/ld+json"]')
- for (let i = 0; i < scripts.length; i++) {
- let jsonld
- if (scripts[i].innerText in parseLDJSONCache) {
- jsonld = parseLDJSONCache[scripts[i].innerText]
- } else {
- try {
- jsonld = JSON.parse(scripts[i].innerText)
- parseLDJSONCache[scripts[i].innerText] = jsonld
- } catch (e) {
- parseLDJSONCache[scripts[i].innerText] = null
- continue
- }
- }
- if (jsonld) {
- if (Array.isArray(jsonld)) {
- data.push(...jsonld)
- } else {
- data.push(jsonld)
- }
- }
- }
- for (let i = 0; i < data.length; i++) {
- try {
- if (data[i] && data[i] && (typeof condition !== 'function' || condition(data[i]))) {
- if (Array.isArray(keys)) {
- const r = []
- for (let j = 0; j < keys.length; j++) {
- r.push(data[i][keys[j]])
- }
- return decodeXmlEntities(r)
- } else if (keys) {
- return decodeXmlEntities(data[i][keys])
- } else if (typeof condition === 'function') {
- return decodeXmlEntities(data[i]) // Return whole object
- }
- }
- } catch (e) {
- continue
- }
- }
- return decodeXmlEntities(data)
- }
- return null
- }
-
- function askFlixsterEMS (emsId) {
- return new Promise(function flixsterEMSRequest (resolve) {
- GM.getValue('flixsterEmsCache', '{}').then(function (s) {
- const flixsterEmsCache = JSON.parse(s)
-
- // Delete algoliaCached values, that are expired
- for (const prop in flixsterEmsCache) {
- if ((new Date()).getTime() - (new Date(flixsterEmsCache[prop].time)).getTime() > cacheExpireAfterHours * 60 * 60 * 1000) {
- delete flixsterEmsCache[prop]
- }
- }
-
- // Check cache or request new content
- if (emsId in flixsterEmsCache) {
- return resolve(flixsterEmsCache[emsId])
- }
- const url = flixsterEMSURL.replace('{emsId}', encodeURIComponent(emsId))
- GM.xmlHttpRequest({
- method: 'GET',
- url,
- onload: function (response) {
- let data = null
- try {
- data = JSON.parse(response.responseText)
- } catch (e) {
- console.error(`${scriptName}: flixster ems JSON Error\nURL: ${url}`)
- console.error(e)
- data = {}
- }
-
- // Save to flixsterEmsCache
- data.time = (new Date()).toJSON()
-
- flixsterEmsCache[emsId] = data
-
- GM.setValue('flixsterEmsCache', JSON.stringify(flixsterEmsCache))
-
- resolve(data)
- },
- onerror: function (response) {
- console.error(`${scriptName}: flixster ems GM.xmlHttpRequest Error: ${response.status}\nURL: ${url}\nResponse:\n${response.responseText}`)
- resolve(null)
- }
- })
- })
- })
- }
- async function addFlixsterEMS (orgData) {
- const flixsterData = await askFlixsterEMS(orgData.emsId)
- if (!flixsterData || !('tomatometer' in flixsterData)) {
- return orgData
- }
- if ('certifiedFresh' in flixsterData.tomatometer && flixsterData.tomatometer.certifiedFresh) {
- orgData.meterClass = 'certified_fresh'
- }
- if ('numReviews' in flixsterData.tomatometer && flixsterData.tomatometer.numReviews) {
- orgData.numReviews = flixsterData.tomatometer.numReviews
- if ('freshCount' in flixsterData.tomatometer && flixsterData.tomatometer.freshCount != null) {
- orgData.freshCount = flixsterData.tomatometer.freshCount
- }
- if ('rottenCount' in flixsterData.tomatometer && flixsterData.tomatometer.rottenCount != null) {
- orgData.rottenCount = flixsterData.tomatometer.rottenCount
- }
- }
- if ('consensus' in flixsterData.tomatometer && flixsterData.tomatometer.consensus) {
- orgData.consensus = flixsterData.tomatometer.consensus
- }
- if ('avgScore' in flixsterData.tomatometer && flixsterData.tomatometer.avgScore != null) {
- orgData.avgScore = flixsterData.tomatometer.avgScore
- }
- if ('userRatingSummary' in flixsterData) {
- if ('scoresCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.scoresCount) {
- orgData.audienceCount = flixsterData.userRatingSummary.scoresCount
- } else if ('dtlScoreCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.dtlScoreCount) {
- orgData.audienceCount = flixsterData.userRatingSummary.dtlScoreCount
- }
- if ('wtsCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.wtsCount) {
- orgData.audienceWantToSee = flixsterData.userRatingSummary.wtsCount
- } else if ('dtlWtsCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.dtlWtsCount) {
- orgData.audienceWantToSee = flixsterData.userRatingSummary.dtlWtsCount
- }
- if ('reviewCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.reviewCount) {
- orgData.audienceReviewCount = flixsterData.userRatingSummary.reviewCount
- }
- if ('avgScore' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.avgScore) {
- orgData.audienceAvgScore = flixsterData.userRatingSummary.avgScore
- }
- }
- return orgData
- }
-
- function updateAlgolia () {
- // Get algolia data from https://www.rottentomatoes.com/
- const algoliaSearch = { aId: null, sId: null }
- if (unsafeWindow.RottenTomatoes && 'thirdParty' in unsafeWindow.RottenTomatoes && 'algoliaSearch' in unsafeWindow.RottenTomatoes.thirdParty) {
- if (typeof (unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.aId) === 'string' && typeof (unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.sId) === 'string') {
- algoliaSearch.aId = unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.aId // x-algolia-application-id
- algoliaSearch.sId = unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.sId // x-algolia-api-key
- }
- }
- if (algoliaSearch.aId) {
- GM.setValue('algoliaSearch', JSON.stringify(algoliaSearch)).then(function () {
- console.debug(`${scriptName}: Updated algoliaSearch: ${JSON.stringify(algoliaSearch)}`)
- })
- } else {
- console.debug(`${scriptName}: algoliaSearch.aId is ${algoliaSearch.aId}`)
- }
- }
-
- function meterBar (data) {
- // Create the "progress" bar with the meter score
- let barColor = 'grey'
- let bgColor = darkTheme ? '#3e3e3e' : '#ECE4B5'
- let color = 'black'
- let width = 0
- let textInside = ''
- let textAfter = ''
-
- if (data.meterClass === 'certified_fresh') {
- barColor = '#C91B22'
- color = 'yellow'
- textInside = emojiStrawberry + ' ' + data.meterScore.toLocaleString() + '%'
- width = data.meterScore || 0
- } else if (data.meterClass === 'fresh') {
- barColor = '#C91B22'
- color = 'white'
- textInside = emojiTomato + ' ' + data.meterScore.toLocaleString() + '%'
- width = data.meterScore || 0
- } else if (data.meterClass === 'rotten') {
- color = 'gray'
- barColor = '#94B13C'
- if (data.meterScore && data.meterScore > 30) {
- textAfter = '<span style="font-size: 15px;padding-top: 2px;display: inline-block;">' + data.meterScore.toLocaleString() + '%</span>'
- textInside = '<span style="font-size:13px">' + emojiGreenApple + '</span>'
- } else {
- textAfter = data.meterScore.toLocaleString() + '% <span style="font-size:13px">' + emojiGreenApple + '</span>'
- }
- width = data.meterScore || 0
- } else {
- bgColor = barColor = '#787878'
- color = 'silver'
- textInside = 'N/A'
- width = 100
- }
-
- let title = 'Critics ' + (typeof data.meterScore === 'number' ? data.meterScore.toLocaleString() : 'N/A') + '% ' + data.meterClass
- let avg = ''
- if ('avgScore' in data) {
- const node = document.createElement('span')
- node.innerHTML = data.consensus
- title += '\nAverage score: ' + data.avgScore.toLocaleString() + ' / 10'
- avg = '<span style="font-weight:bolder">' + data.avgScore.toLocaleString() + '</span>/10'
- }
- if ('numReviews' in data && typeof data.numReviews === 'number') {
- title += ' from ' + data.numReviews.toLocaleString() + ' reviews'
- if ('freshCount' in data && data.numReviews > 0) {
- const p = parseInt(100 * parseFloat(data.freshCount) / parseFloat(data.numReviews))
- title += '\n' + data.freshCount.toLocaleString() + '/' + data.numReviews.toLocaleString() + ' ' + p + '% fresh reviews'
- }
- if ('rottenCount' in data) {
- const p = parseInt(100 * parseFloat(data.rottenCount) / parseFloat(data.numReviews))
- title += '\n' + data.rottenCount.toLocaleString() + '/' + data.numReviews.toLocaleString() + ' ' + p + '% rotten reviews'
- }
- }
- if ('consensus' in data) {
- const node = document.createElement('span')
- node.innerHTML = data.consensus
- title += '\n' + node.textContent
- }
- return '<div title="' + title + '" style="cursor:help;">' +
- '<div style="float:left; margin-top:1px; width:100px; overflow: hidden;height: 20px;background-color: ' + bgColor + ';color: ' + color + ';text-align:center; border-radius: 4px;box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);">' +
- '<div style="width:' + width + '%; background-color: ' + barColor + '; color: ' + color + '; font-size:14px; font-weight:bold; text-align:center; float:left; height: 100%;line-height: 20px;box-shadow: inset 0 -1px 0 rgba(0,0,0,0.15);transition: width 0.6s ease;">' +
- textInside +
- '</div>' +
- textAfter +
- '</div>' +
- '<div style="float:left; padding: 3px 0px 0px 3px;">' + avg + '</div>' +
- '<div style="clear:left;"></div>' +
- '</div>'
- }
- function audienceBar (data) {
- // Create the "progress" bar with the audience score
- if (!('audienceScore' in data) || data.audienceScore === null) {
- return ''
- }
-
- let barColor = 'grey'
- let bgColor = darkTheme ? '#3e3e3e' : '#ECE4B5'
- let color = 'black'
- let width = 0
- let textInside = ''
- let textAfter = ''
- let avg = ''
-
- if (data.audienceClass === 'red_popcorn') {
- barColor = '#C91B22'
- color = data.audienceScore > 94 ? 'yellow' : 'white'
- textInside = emojiPopcorn + ' ' + data.audienceScore.toLocaleString() + '%'
- width = data.audienceScore
- } else if (data.audienceClass === 'green_popcorn') {
- color = 'gray'
- barColor = '#94B13C'
- if (data.audienceScore > 30) {
- textAfter = '<span style="font-size: 15px;padding-top: 2px;display: inline-block;">' + data.audienceScore.toLocaleString() + '%</span>'
- textInside = '<span style="font-size:13px">' + emojiGreenSalad + '</span>'
- } else {
- textAfter = data.audienceScore.toLocaleString() + '% <span style="font-size:13px">' + emojiNauseated + '</span>'
- }
- width = data.audienceScore
- } else {
- bgColor = barColor = '#787878'
- color = 'silver'
- textInside = 'N/A'
- width = 100
- }
-
- let title = 'Audience ' + (typeof data.audienceScore === 'number' ? data.audienceScore.toLocaleString() : 'N/A') + '% ' + data.audienceClass
- const titleLine2 = []
- if ('audienceCount' in data && typeof data.audienceCount === 'number') {
- titleLine2.push(data.audienceCount.toLocaleString() + ' Votes')
- }
- if ('audienceReviewCount' in data) {
- titleLine2.push(data.audienceReviewCount.toLocaleString() + ' Reviews')
- }
- if ('audienceAvgScore' in data && typeof data.audienceAvgScore === 'number') {
- titleLine2.push('Average score: ' + data.audienceAvgScore.toLocaleString() + ' / 5 stars')
- avg = '<span style="font-weight:bolder">' + data.audienceAvgScore.toLocaleString() + '</span>/5'
- }
- if ('audienceWantToSee' in data && typeof data.audienceWantToSee === 'number') {
- titleLine2.push(data.audienceWantToSee.toLocaleString() + ' want to see')
- }
-
- title = title + (titleLine2 ? ('\n' + titleLine2.join('\n')) : '')
- return '<div title="' + title + '" style="cursor:help;">' +
- '<div style="float:left; margin-top:1px; width:100px; overflow: hidden;height: 20px;background-color: ' + bgColor + ';color: ' + color + ';text-align:center; border-radius: 4px;box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);">' +
- '<div style="width:' + width + '%; background-color: ' + barColor + '; color: ' + color + '; font-size:14px; font-weight:bold; text-align:center; float:left; height: 100%;line-height: 20px;box-shadow: inset 0 -1px 0 rgba(0,0,0,0.15);transition: width 0.6s ease;">' +
- textInside +
- '</div>' +
- textAfter +
- '</div>' +
- '<div style="float:left; padding: 3px 0px 0px 3px;">' + avg + '</div>' +
- '<div style="clear:left;"></div>' +
- '</div>'
- }
-
- const current = {
- type: null,
- query: null,
- year: null
- }
-
- async function loadMeter (query, type, year) {
- // Load data from rotten tomatoes search API or from cache
-
- current.type = type
- current.query = query
- current.year = year
-
- const algoliaCache = JSON.parse(await GM.getValue('algoliaCache', '{}'))
-
- // Delete algoliaCached values, that are expired
- for (const prop in algoliaCache) {
- if ((new Date()).getTime() - (new Date(algoliaCache[prop].time)).getTime() > cacheExpireAfterHours * 60 * 60 * 1000) {
- delete algoliaCache[prop]
- }
- }
-
- const algoliaSearch = JSON.parse(await GM.getValue('algoliaSearch', '{}'))
-
- // Check cache or request new content
- if (query in algoliaCache) {
- // Use cached response
- console.debug(`${scriptName}: Use cached algolia response`)
- handleAlgoliaResponse(algoliaCache[query])
- } else if ('aId' in algoliaSearch && 'sId' in algoliaSearch) {
- // Use algolia.net API
- const url = algoliaURL.replace('{domain}', algoliaSearch.aId.toLowerCase()).replace('{aId}', encodeURIComponent(algoliaSearch.aId)).replace('{sId}', encodeURIComponent(algoliaSearch.sId)).replace('{agent}', encodeURIComponent(algoliaAgent))
- GM.xmlHttpRequest({
- method: 'POST',
- url,
- data: '{"requests":[{"indexName":"content_rt","query":"' + query.replace('"', '') + '","params":"filters=isEmsSearchable%20%3D%201&hitsPerPage=20"}]}',
- onload: function (response) {
- // Save to algoliaCache
- response.time = (new Date()).toJSON()
-
- // Chrome fix: Otherwise JSON.stringify(cache) omits responseText
- const newobj = {}
- for (const key in response) {
- newobj[key] = response[key]
- }
- newobj.responseText = response.responseText
-
- algoliaCache[query] = newobj
-
- GM.setValue('algoliaCache', JSON.stringify(algoliaCache))
-
- handleAlgoliaResponse(response)
- },
- onerror: function (response) {
- console.error(`${scriptName}: algoliaSearch GM.xmlHttpRequest Error: ${response.status}\nURL: ${url}\nResponse:\n${response.responseText}`)
- }
- })
- } else {
- console.error(`${scriptName}: algoliaSearch not configured`)
- window.alert(scriptName + ' userscript\n\nYou need to visit www.rottentomatoes.com at least once before the script can work.\n\nThe script needs to read some API keys from the website.')
- showMeter('ALGOLIA_NOT_CONFIGURED', new Date())
- }
- }
-
- function matchQuality (title, year, currentSet) {
- if (title === current.query && year === current.year) {
- return 104 + year
- }
- if (title.toLowerCase() === current.query.toLowerCase() && year === current.year) {
- return 103 + year
- }
- if (title === current.query && current.year) {
- return 102 - Math.abs(year - current.year)
- }
- if (title.toLowerCase() === current.query.toLowerCase() && current.year) {
- return 101 - Math.abs(year - current.year)
- }
- if (title.replace(/\(.+\)/, '').trim() === current.query && current.year) {
- return 100 - Math.abs(year - current.year)
- }
- if (title === current.query) {
- return 8
- }
- if (title.replace(/\(.+\)/, '').trim() === current.query) {
- return 7
- }
- if (title.startsWith(current.query)) {
- return 6
- }
- if (current.query.indexOf(title) !== -1) {
- return 5
- }
- if (title.indexOf(current.query) !== -1) {
- return 4
- }
- if (current.query.toLowerCase().indexOf(title.toLowerCase()) !== -1) {
- return 3
- }
- if (title.toLowerCase().indexOf(current.query.toLowerCase()) !== -1) {
- return 2
- }
- const titleSet = new Set(title.replace(/[^a-z ]/gi, ' ').split(' '))
- const score = intersection(titleSet, currentSet).size - 20
- if (year === current.year) {
- return score + 1
- }
- return score
- }
-
- async function handleAlgoliaResponse (response) {
- // Handle GM.xmlHttpRequest response
- const rawData = JSON.parse(response.responseText)
-
- // Filter according to type
- const hits = rawData.results[0].hits.filter(hit => hit.type === current.type)
-
- // Change data structure
- const arr = []
-
- hits.forEach(function (hit) {
- const result = {
- name: hit.title,
- year: parseInt(hit.releaseYear),
- url: '/' + (current.type === 'tv' ? 'tv' : 'm') + '/' + ('vanity' in hit ? hit.vanity : hit.title.toLowerCase()),
- meterClass: null,
- meterScore: null,
- audienceClass: null,
- audienceScore: null,
- emsId: hit.emsId
- }
- if ('rottenTomatoes' in hit) {
- if ('criticsIconUrl' in hit.rottenTomatoes) {
- result.meterClass = hit.rottenTomatoes.criticsIconUrl.match(/\/(\w+)\.png/)[1]
- }
- if ('criticsScore' in hit.rottenTomatoes) {
- result.meterScore = hit.rottenTomatoes.criticsScore
- }
- if ('audienceIconUrl' in hit.rottenTomatoes) {
- result.audienceClass = hit.rottenTomatoes.audienceIconUrl.match(/\/(\w+)\.png/)[1]
- }
- if ('audienceScore' in hit.rottenTomatoes) {
- result.audienceScore = hit.rottenTomatoes.audienceScore
- }
- if ('certifiedFresh' in hit.rottenTomatoes && hit.rottenTomatoes.certifiedFresh) {
- result.meterClass = 'certified_fresh'
- }
- }
- arr.push(result)
- })
-
- // Sort results by closest match
- const currentSet = new Set(current.query.replace(/[^a-z ]/gi, ' ').split(' '))
- arr.sort(function (a, b) {
- if (!Object.prototype.hasOwnProperty.call(a, 'matchQuality')) {
- a.matchQuality = matchQuality(a.name, a.year, currentSet)
- }
- if (!Object.prototype.hasOwnProperty.call(b, 'matchQuality')) {
- b.matchQuality = matchQuality(b.name, b.year, currentSet)
- }
-
- return b.matchQuality - a.matchQuality
- })
-
- if (arr.length > 0 && arr[0].meterScore) {
- // Get more details for first result
- arr[0] = await addFlixsterEMS(arr[0])
- }
-
- if (arr) {
- showMeter(arr, new Date(response.time))
- } else {
- console.debug(`${scriptName}: No results for ${current.query}`)
- }
- }
-
- function showMeter (arr, time) {
- // Show a small box in the right lower corner
- $('#mcdiv321rotten').remove()
- let main, div
- div = main = $('<div id="mcdiv321rotten"></div>').appendTo(document.body)
- div.css({
- position: 'fixed',
- bottom: 0,
- right: 0,
- minWidth: 100,
- maxWidth: 400,
- maxHeight: '95%',
- overflow: 'auto',
- backgroundColor: darkTheme ? '#262626' : 'white',
- border: darkTheme ? '2px solid #444' : '2px solid #bbb',
- borderRadius: ' 6px',
- boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
- color: darkTheme ? 'white' : 'black',
- padding: ' 3px',
- zIndex: '5010001',
- fontFamily: 'Helvetica,Arial,sans-serif'
- })
-
- const CSS = `<style>
- #mcdiv321rotten {
- transition:bottom 0.7s, height 0.5s;
- }
- </style>`
-
- $(CSS).appendTo(div)
-
- if (arr === 'ALGOLIA_NOT_CONFIGURED') {
- $('<div>You need to visit <a href="https://www.rottentomatoes.com/">www.rottentomatoes.com</a> at least once to enable the script.</div>').appendTo(main)
- return
- }
-
- // First result
- $('<div class="firstResult"><a style="font-size:small; color:#136CB2; " href="' + baseURL + arr[0].url + '">' + arr[0].name + ' (' + arr[0].year + ')</a>' + meterBar(arr[0]) + audienceBar(arr[0]) + '</div>').appendTo(main)
-
- // Shall the following results be collapsed by default?
- if ((arr.length > 1 && arr[0].matchQuality > 10) || arr.length > 10) {
- $('<span style="color:gray;font-size: x-small">More results...</span>').appendTo(main).click(function () { more.css('display', 'block'); this.parentNode.removeChild(this) })
- const more = div = $('<div style="display:none"></div>').appendTo(main)
- }
-
- // More results
- for (let i = 1; i < arr.length; i++) {
- $('<div><a style="font-size:small; color:#136CB2; " href="' + baseURL + arr[i].url + '">' + arr[i].name + ' (' + arr[i].year + ')</a>' + meterBar(arr[i]) + audienceBar(arr[i]) + '</div>').appendTo(div)
- }
-
- // Footer
- const sub = $('<div></div>').appendTo(main)
- $('<time style="color:#b6b6b6; font-size: 11px;" datetime="' + time + '" title="' + time.toLocaleTimeString() + ' ' + time.toLocaleDateString() + '">' + minutesSince(time) + '</time>').appendTo(sub)
- $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + baseURLOpenTab.replace('{query}', encodeURIComponent(current.query)) + '" title="Open Rotten Tomatoes">@rottentomatoes.com</a>').appendTo(sub)
- $('<span title="Hide me" style="cursor:pointer; float:right; color:#b6b6b6; font-size: 11px; padding-left:5px;padding-top:3px">❎</span>').appendTo(sub).click(function () {
- document.body.removeChild(this.parentNode.parentNode)
- })
- }
-
- const Always = () => true
- const sites = {
- googleplay: {
- host: ['play.google.com'],
- condition: Always,
- products: [
- {
- condition: () => ~document.location.href.indexOf('/movies/details/'),
- type: 'movie',
- data: () => document.querySelector('*[itemprop=name]').textContent
- }
- ]
- },
- imdb: {
- host: ['imdb.com'],
- condition: () => !~document.location.pathname.indexOf('/mediaviewer') && !~document.location.pathname.indexOf('/mediaindex') && !~document.location.pathname.indexOf('/videoplayer'),
- products: [
- {
- condition: function () {
- const e = document.querySelector("meta[property='og:type']")
- if (e && e.content === 'video.movie') {
- return true
- } else if (document.querySelector('[data-testid="hero__pageTitle"]') && !document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
- return true
- }
- return false
- },
- type: 'movie',
- data: async function () {
- let year = null
- let ld = null
- if (document.querySelector('script[type="application/ld+json"]')) {
- ld = parseLDJSON(['name', 'alternateName', 'datePublished'])
- if (ld.length > 2) {
- year = parseInt(ld[2].match(/\d{4}/)[0])
- }
- }
-
- const pageNotEnglish = document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')
- const pageNotMovieHomePage = !document.title.match(/(.+?)\s+(\((\d+)\))? - IMDb/)
-
- // If the page is not in English or the browser is not in English, request page in English.
- // Then the title in <h1> will be the English title and Metacritic always uses the English title.
- if (pageNotEnglish || pageNotMovieHomePage) {
- // Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
- const imdbID = document.location.pathname.match(/\/title\/(\w+)/)[1]
- const homePageUrl = 'https://www.imdb.com/title/' + imdbID + '/?ref_=nv_sr_1'
- const langM = document.cookie.match(/lc-main=([^;]+)/)
- const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
- document.cookie = 'lc-main=en-US'
- const response = await asyncRequest({
- url: homePageUrl,
- headers: {
- 'Accept-Language': 'en-US,en'
- }
- }).catch(function (response) {
- console.warn('ShowRottentomatoes: Error imdb02\nurl=' + homePageUrl + '\nstatus=' + response.status)
- })
- document.cookie = 'lc-main=' + langBefore
- // Extract <h1> title
- const parts = response.responseText.split('</span></h1>')[0].split('>')
- const title = parts[parts.length - 1]
- if (!year) {
- // extract year
- const yearM = response.responseText.match(/href="\/title\/\w+\/releaseinfo.*">(\d{4})<\/a>/)
- if (yearM) {
- year = yearM[1]
- }
- }
- console.debug('ShowRottentomatoes: Movie title from English page:', title, year)
- return [title, year]
- } else if (ld) {
- if (ld.length > 1 && ld[1]) {
- console.debug('ShowRottentomatoes: Movie ld+json alternateName', ld[1], year)
- return [ld[1], year]
- }
- console.debug('ShowRottentomatoes: Movie ld+json name', ld[0], year)
- return [ld[0], year]
- } else {
- const m = document.title.match(/(.+?)\s+(\((\d+)\))? - /)
- console.debug('ShowRottentomatoes: Movie <title>', [m[1], m[3]])
- return [m[1], parseInt(m[3])]
- }
- }
- },
- {
- condition: function () {
- const e = document.querySelector("meta[property='og:type']")
- if (e && e.content === 'video.tv_show') {
- return true
- } else if (document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
- return true
- }
- return false
- },
- type: 'tv',
- data: async function () {
- let year = null
- let ld = null
- if (document.querySelector('script[type="application/ld+json"]')) {
- ld = parseLDJSON(['name', 'alternateName', 'datePublished'])
- if (ld.length > 2) {
- year = parseInt(ld[2].match(/\d{4}/)[0])
- }
- }
-
- const pageNotEnglish = document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')
- const pageNotMovieHomePage = !document.title.match(/(.+?)\s+\(.+(\d{4})–.{0,4}\) - IMDb/)
-
- // If the page is not in English or the browser is not in English, request page in English.
- // Then the title in <h1> will be the English title and Metacritic always uses the English title.
- if (pageNotEnglish || pageNotMovieHomePage) {
- const imdbID = document.location.pathname.match(/\/title\/(\w+)/)[1]
- const homePageUrl = 'https://www.imdb.com/title/' + imdbID + '/?ref_=nv_sr_1'
- // Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
- const langM = document.cookie.match(/lc-main=([^;]+)/)
- const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
- document.cookie = 'lc-main=en-US'
- const response = await asyncRequest({
- url: homePageUrl,
- headers: {
- 'Accept-Language': 'en-US,en'
- }
- }).catch(function (response) {
- console.warn('ShowRottentomatoes: Error imdb03\nurl=' + homePageUrl + '\nstatus=' + response.status)
- })
- document.cookie = 'lc-main=' + langBefore
- // Extract <h1> title
- const parts = response.responseText.split('</span></h1>')[0].split('>')
- const title = parts[parts.length - 1]
- if (!year) {
- // extract year
- const yearM = response.responseText.match(/href="\/title\/\w+\/releaseinfo.*">(\d{4})/)
- if (yearM) {
- year = yearM[1]
- }
- }
- console.debug('ShowRottentomatoes: TV title from English page:', title, year)
- return [title, year]
- } else if (ld) {
- if (ld.length > 1 && ld[1]) {
- console.debug('ShowRottentomatoes: TV ld+json alternateName', ld[1], year)
- return [ld[1], year]
- }
- console.debug('ShowRottentomatoes: TV ld+json name', ld[0], year)
- return [ld[0], year]
- } else {
- const m = document.title.match(/(.+?)\s+\(.+(\d{4}).+/)
- console.debug('ShowRottentomatoes: TV <title>', [m[1], m[2]])
- return [m[1], parseInt(m[2])]
- }
- }
- }
- ]
- },
- 'tv.com': {
- host: ['www.tv.com'],
- condition: () => document.querySelector("meta[property='og:type']"),
- products: [{
- condition: () => document.querySelector("meta[property='og:type']").content === 'tv_show' && document.querySelector('h1[data-name]'),
- type: 'tv',
- data: () => document.querySelector('h1[data-name]').dataset.name
- }]
- },
- metacritic: {
- host: ['www.metacritic.com'],
- condition: () => document.querySelector("meta[property='og:type']"),
- products: [{
- condition: () => document.querySelector("meta[property='og:type']").content === 'video.movie',
- type: 'movie',
- data: function () {
- let year = null
- if (document.querySelector('.release_year')) {
- year = parseInt(document.querySelector('.release_year').firstChild.textContent)
- } else if (document.querySelector('.release_data .data')) {
- year = document.querySelector('.release_data .data').textContent.match(/(\d{4})/)[1]
- }
-
- return [document.querySelector("meta[property='og:title']").content, year]
- }
- },
- {
- condition: () => document.querySelector("meta[property='og:type']").content === 'video.tv_show',
- type: 'tv',
- data: function () {
- let title = document.querySelector("meta[property='og:title']").content
- let year = null
- if (title.match(/\s\(\d{4}\)$/)) {
- year = parseInt(title.match(/\s\((\d{4})\)$/)[1])
- title = title.replace(/\s\(\d{4}\)$/, '') // Remove year
- } else if (document.querySelector('.release_date')) {
- year = document.querySelector('.release_date').textContent.match(/(\d{4})/)[1]
- }
-
- return [title, year]
- }
- }
- ]
- },
- serienjunkies: {
- host: ['www.serienjunkies.de'],
- condition: Always,
- products: [{
- condition: () => document.getElementById('serienlinksbreit2aktuell'),
- type: 'tv',
- data: () => document.querySelector('h1').textContent.trim()
- },
- {
- condition: () => document.location.pathname.search(/vod\/film\/.{3,}/) !== -1,
- type: 'movie',
- data: () => document.querySelector('h1').textContent.trim()
- }]
- },
- amazon: {
- host: ['amazon.'],
- condition: Always,
- products: [
- {
- condition: () => (document.querySelector('[data-automation-id=title]') && (
- document.getElementsByClassName('av-season-single').length ||
- document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
- document.getElementById('tab-selector-episodes') ||
- document.getElementById('av-droplist-av-atf-season-selector')
- )),
- type: 'tv',
- data: () => document.querySelector('[data-automation-id=title]').textContent.trim()
- },
- {
- condition: () => ((
- document.getElementsByClassName('av-season-single').length ||
- document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
- document.getElementById('tab-selector-episodes') ||
- document.getElementById('av-droplist-av-atf-season-selector')
- ) && Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).some((x) => x != null)),
- type: 'tv',
- data: () => Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).filter((x) => x != null)[0][1]
- },
- {
- condition: () => document.querySelector('[data-automation-id=title]'),
- type: 'movie',
- data: () => document.querySelector('[data-automation-id=title]').textContent.trim().replace(/\[.{1,8}\]/, '')
- },
- {
- condition: () => document.querySelector('#watchNowContainer a[href*="/gp/video/"]'),
- type: 'movie',
- data: () => document.getElementById('productTitle').textContent.trim()
- }
- ]
- },
- BoxOfficeMojo: {
- host: ['boxofficemojo.com'],
- condition: () => Always,
- products: [
- {
- condition: () => document.location.pathname.startsWith('/release/'),
- type: 'movie',
- data: function () {
- let year = null
- const cells = document.querySelectorAll('#body .mojo-summary-values .a-section span')
- for (let i = 0; i < cells.length; i++) {
- if (~cells[i].innerText.indexOf('Release Date')) {
- year = parseInt(cells[i].nextElementSibling.textContent.match(/\d{4}/)[0])
- break
- }
- }
- return [document.querySelector('meta[name=title]').content, year]
- }
- },
- {
- condition: () => ~document.location.search.indexOf('id=') && document.querySelector('#body table:nth-child(2) tr:first-child b'),
- type: 'movie',
- data: function () {
- let year = null
- try {
- const tds = document.querySelectorAll('#body table:nth-child(2) tr:first-child table table table td')
- for (let i = 0; i < tds.length; i++) {
- if (~tds[i].innerText.indexOf('Release Date')) {
- year = parseInt(tds[i].innerText.match(/\d{4}/)[0])
- break
- }
- }
- } catch (e) { }
- return [document.querySelector('#body table:nth-child(2) tr:first-child b').firstChild.textContent, year]
- }
- }]
- },
- AllMovie: {
- host: ['allmovie.com'],
- condition: () => document.querySelector('h2[itemprop=name].movie-title'),
- products: [{
- condition: () => document.querySelector('h2[itemprop=name].movie-title'),
- type: 'movie',
- data: () => document.querySelector('h2[itemprop=name].movie-title').firstChild.textContent.trim()
- }]
- },
- 'en.wikipedia': {
- host: ['en.wikipedia.org'],
- condition: Always,
- products: [{
- condition: function () {
- if (!document.querySelector('.infobox .summary')) {
- return false
- }
- const r = /\d\d\d\d films/
- return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
- },
- type: 'movie',
- data: () => document.querySelector('.infobox .summary').firstChild.textContent
- },
- {
- condition: function () {
- if (!document.querySelector('.infobox .summary')) {
- return false
- }
- const r = /television series/
- return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
- },
- type: 'tv',
- data: () => document.querySelector('.infobox .summary').firstChild.textContent
- }]
- },
- fandango: {
- host: ['fandango.com'],
- condition: () => document.querySelector("meta[property='og:title']"),
- products: [{
- condition: Always,
- type: 'movie',
- data: () => document.querySelector("meta[property='og:title']").content.match(/(.+?)\s+\(\d{4}\)/)[1].trim()
- }]
- },
- themoviedb: {
- host: ['themoviedb.org'],
- condition: () => document.querySelector("meta[property='og:type']"),
- products: [{
- condition: () => document.querySelector("meta[property='og:type']").content === 'movie' ||
- document.querySelector("meta[property='og:type']").content === 'video.movie',
- type: 'movie',
- data: function () {
- let year = null
- try {
- year = parseInt(document.querySelector('.release_date').innerText.match(/\d{4}/)[0])
- } catch (e) {}
-
- return [document.querySelector("meta[property='og:title']").content, year]
- }
- },
- {
- condition: () => document.querySelector("meta[property='og:type']").content === 'tv' ||
- document.querySelector("meta[property='og:type']").content === 'tv_series' ||
- document.querySelector("meta[property='og:type']").content.indexOf('tv_show') !== -1,
- type: 'tv',
- data: () => document.querySelector("meta[property='og:title']").content
- }]
- },
- letterboxd: {
- host: ['letterboxd.com'],
- condition: () => unsafeWindow.filmData && 'name' in unsafeWindow.filmData,
- products: [{
- condition: Always,
- type: 'movie',
- data: () => [unsafeWindow.filmData.name, unsafeWindow.filmData.releaseYear]
- }]
- },
- TVmaze: {
- host: ['tvmaze.com'],
- condition: () => document.querySelector('h1'),
- products: [{
- condition: Always,
- type: 'tv',
- data: () => document.querySelector('h1').firstChild.textContent
- }]
- },
- TVGuide: {
- host: ['tvguide.com'],
- condition: Always,
- products: [{
- condition: () => document.location.pathname.startsWith('/tvshows/'),
- type: 'tv',
- data: function () {
- if (document.querySelector('meta[itemprop=name]')) {
- return document.querySelector('meta[itemprop=name]').content
- } else {
- return document.querySelector("meta[property='og:title']").content.split('|')[0]
- }
- }
- }]
- },
- followshows: {
- host: ['followshows.com'],
- condition: Always,
- products: [{
- condition: () => document.querySelector("meta[property='og:type']").content === 'video.tv_show',
- type: 'tv',
- data: () => document.querySelector("meta[property='og:title']").content
- }]
- },
- TheTVDB: {
- host: ['thetvdb.com'],
- condition: Always,
- products: [{
- condition: () => document.location.pathname.startsWith('/series/'),
- type: 'tv',
- data: () => document.getElementById('series_title').firstChild.textContent.trim()
- },
- {
- condition: () => document.location.pathname.startsWith('/movies/'),
- type: 'movie',
- data: () => document.getElementById('series_title').firstChild.textContent.trim()
- }]
- },
- TVNfo: {
- host: ['tvnfo.com'],
- condition: () => document.querySelector('#title #name'),
- products: [{
- condition: Always,
- type: 'tv',
- data: function () {
- const years = document.querySelector('#title #years').textContent.trim()
- const title = document.querySelector('#title #name').textContent.replace(years, '').trim()
- let year = null
- if (years) {
- try {
- year = years.match(/\d{4}/)[0]
- } catch (e) {}
- }
- return [title, year]
- }
- }]
- },
- nme: {
- host: ['nme.com'],
- condition: () => document.location.pathname.startsWith('/reviews/'),
- products: [{
- condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/film-reviews"]'),
- type: 'movie',
- data: function () {
- let year = null
- try {
- year = parseInt(document.querySelector('*[itemprop=datePublished]').content.match(/\d{4}/)[0])
- } catch (e) {}
-
- try {
- return [document.title.match(/[‘'](.+?)[’']/)[1], year]
- } catch (e) {
- try {
- return [document.querySelector('h1.tdb-title-text').textContent.match(/[‘'](.+?)[’']/)[1], year]
- } catch (e) {
- return [document.querySelector('h1').textContent.match(/:\s*(.+)/)[1].trim(), year]
- }
- }
- }
- },
- {
- condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/tv-reviews"]'),
- type: 'tv',
- data: () => document.querySelector('h1.tdb-title-text').textContent.match(/‘(.+?)’/)[1]
- }]
- },
- itunes: {
- host: ['itunes.apple.com'],
- condition: Always,
- products: [{
- condition: () => ~document.location.href.indexOf('/movie/'),
- type: 'movie',
- data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
- },
- {
- condition: () => ~document.location.href.indexOf('/tv-season/'),
- type: 'tv',
- data: function () {
- let name = parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
- if (~name.indexOf(', Season')) {
- name = name.split(', Season')[0]
- }
- return name
- }
- }]
- },
- epguides: {
- host: ['epguides.com'],
- condition: () => document.getElementById('eplist'),
- products: [{
- condition: () => document.getElementById('eplist') && document.querySelector('.center.titleblock h2'),
- type: 'tv',
- data: () => document.querySelector('.center.titleblock h2').textContent.trim()
- }]
- },
- ComedyCentral: {
- host: ['cc.com'],
- condition: () => document.location.pathname.startsWith('/shows/'),
- products: [{
- condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:title']"),
- type: 'tv',
- data: () => document.querySelector("meta[property='og:title']").content.replace('| Comedy Central', '').trim()
- },
- {
- condition: () => document.location.pathname.split('/').length === 3 && document.title.match(/(.+?)\s+-\s+Series/),
- type: 'tv',
- data: () => document.title.match(/(.+?)\s+-\s+Series/)[1]
- }]
- },
- AMC: {
- host: ['amc.com'],
- condition: () => document.location.pathname.startsWith('/shows/'),
- products: [
- {
- condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:type']") && document.querySelector("meta[property='og:type']").content.indexOf('tv_show') !== -1,
- type: 'tv',
- data: () => document.querySelector('.video-card-description h1').textContent.trim()
- }]
- },
- AMCplus: {
- host: ['amcplus.com'],
- condition: () => Always,
- products: [
- {
- condition: () => document.title.match(/Watch .+? |/),
- type: 'tv',
- data: () => document.title.match(/Watch (.+?) |/)[1].trim()
- }]
- },
- RlsBB: {
- host: ['rlsbb.ru'],
- condition: () => document.querySelectorAll('.post').length === 1,
- products: [
- {
- condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/movies/"]'),
- type: 'movie',
- data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+\d{4}/)[1].trim()
- },
- {
- condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/tv-shows/"]'),
- type: 'tv',
- data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+S\d{2}/)[1].trim()
- }]
- },
- showtime: {
- host: ['sho.com'],
- condition: Always,
- products: [
- {
- condition: () => parseLDJSON('@type') === 'Movie',
- type: 'movie',
- data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
- },
- {
- condition: () => parseLDJSON('@type') === 'TVSeries',
- type: 'tv',
- data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
- }]
- },
- gog: {
- host: ['www.gog.com'],
- condition: () => document.querySelector('.productcard-basics__title'),
- products: [{
- condition: () => document.location.pathname.split('/').length > 2 && (
- document.location.pathname.split('/')[1] === 'movie' ||
- document.location.pathname.split('/')[2] === 'movie'),
- type: 'movie',
- data: () => document.querySelector('.productcard-basics__title').textContent
- }]
- },
- psapm: {
- host: ['psa.wf'],
- condition: Always,
- products: [
- {
- condition: () => document.location.pathname.startsWith('/movie/'),
- type: 'movie',
- data: function () {
- const title = document.querySelector('h1').textContent.trim()
- const m = title.match(/(.+)\((\d+)\)$/)
- if (m) {
- return [m[1].trim(), parseInt(m[2])]
- } else {
- return title
- }
- }
- },
- {
- condition: () => document.location.pathname.startsWith('/tv-show/'),
- type: 'tv',
- data: () => document.querySelector('h1').textContent.trim()
- }
- ]
- },
- 'save.tv': {
- host: ['save.tv'],
- condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
- products: [
- {
- condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
- type: 'movie',
- data: function () {
- let title = null
- if (document.querySelector("span[data-bind='text:OrigTitle']")) {
- title = document.querySelector("span[data-bind='text:OrigTitle']").textContent
- } else {
- title = document.querySelector("h2[data-bind='text:Title']").textContent
- }
- let year = null
- if (document.querySelector("span[data-bind='text:ProductionYear']")) {
- year = parseInt(document.querySelector("span[data-bind='text:ProductionYear']").textContent)
- }
- return [title, year]
- }
- }
- ]
- },
- wikiwand: {
- host: ['www.wikiwand.com'],
- condition: Always,
- products: [{
- condition: function () {
- const title = document.querySelector('h1').textContent.toLowerCase()
- const subtitle = document.querySelector('h2[class*="subtitle"]') ? document.querySelector('h2[class*="subtitle"]').textContent.toLowerCase() : ''
- if (title.indexOf('film') === -1 && !subtitle) {
- return false
- }
- return title.indexOf('film') !== -1 ||
- subtitle.indexOf('film') !== -1 ||
- subtitle.indexOf('movie') !== -1
- },
- type: 'movie',
- data: () => document.querySelector('h1').textContent.replace(/\((\d{4} )?film\)/i, '').trim()
- },
- {
- condition: function () {
- const title = document.querySelector('h1').textContent.toLowerCase()
- const subtitle = document.querySelector('h2[class*="subtitle"]') ? document.querySelector('h2[class*="subtitle"]').textContent.toLowerCase() : ''
- if (title.indexOf('tv series') === -1 && !subtitle) {
- return false
- }
- return title.indexOf('tv series') !== -1 ||
- subtitle.indexOf('television') !== -1 ||
- subtitle.indexOf('tv series') !== -1
- },
- type: 'tv',
- data: () => document.querySelector('h1').textContent.replace(/\(tv series\)/i, '').trim()
- }]
- },
- trakt: {
- host: ['trakt.tv'],
- condition: Always,
- products: [
- {
- condition: () => document.location.pathname.startsWith('/movies/'),
- type: 'movie',
- data: function () {
- const title = Array.from(document.querySelector('.summary h1').childNodes).filter(node => node.nodeType === node.TEXT_NODE).map(node => node.textContent).join(' ').trim()
- const year = document.querySelector('.summary h1 .year').textContent
- return [title, year]
- }
- },
- {
- condition: () => document.location.pathname.startsWith('/shows/'),
- type: 'tv',
- data: () => Array.from(document.querySelector('.summary h1').childNodes).filter(node => node.nodeType === node.TEXT_NODE).map(node => node.textContent).join(' ').trim()
- }
- ]
- }
- }
-
- async function main () {
- let dataFound = false
-
- for (const name in sites) {
- const site = sites[name]
- if (site.host.some(function (e) { return ~this.indexOf(e) || e === '*' }, document.location.hostname) && site.condition()) {
- for (let i = 0; i < site.products.length; i++) {
- if (site.products[i].condition()) {
- // Try to retrieve item name from page
- let data
- try {
- data = await site.products[i].data()
- } catch (e) {
- data = false
- console.error(`${scriptName}: Error in data() of site='${name}', type='${site.products[i].type}'`)
- console.error(e)
- }
- if (data) {
- if (Array.isArray(data)) {
- if (data[1]) {
- loadMeter(data[0].trim(), site.products[i].type, parseInt(data[1]))
- } else {
- loadMeter(data[0].trim(), site.products[i].type)
- }
- } else {
- loadMeter(data.trim(), site.products[i].type)
- }
- dataFound = true
- }
- break
- }
- }
- break
- }
- }
- return dataFound
- }
-
- async function adaptForMetaScript () {
- // Move this container above the meta container if the meta container is on the right side
- const rottenC = document.getElementById('mcdiv321rotten')
- const metaC = document.getElementById('mcdiv123')
-
- if (!metaC || !rottenC) {
- return
- }
- const rottenBounds = rottenC.getBoundingClientRect()
-
- let bottom = 0
- if (metaC) {
- const metaBounds = metaC.getBoundingClientRect()
- if (Math.abs(metaBounds.right - rottenBounds.right) < 20 && metaBounds.top > 20) {
- bottom += metaBounds.height
- }
- }
-
- if (bottom > 0) {
- rottenC.style.bottom = bottom + 'px'
- }
- }
-
- (async function () {
- if (document.location.href === 'https://www.rottentomatoes.com/') {
- updateAlgolia()
- }
-
- const firstRunResult = await main()
- let lastLoc = document.location.href
- let lastContent = document.body.innerText
- let lastCounter = 0
- async function newpage () {
- if (lastContent === document.body.innerText && lastCounter < 15) {
- window.setTimeout(newpage, 500)
- lastCounter++
- } else {
- lastContent = document.body.innerText
- lastCounter = 0
- const re = await main()
- if (!re) { // No page matched or no data found
- window.setTimeout(newpage, 1000)
- }
- }
- }
- window.setInterval(function () {
- adaptForMetaScript()
- if (document.location.href !== lastLoc) {
- lastLoc = document.location.href
- $('#mcdiv321rotten').remove()
-
- window.setTimeout(newpage, 1000)
- }
- }, 500)
-
- if (!firstRunResult) {
- // Initial run had no match, let's try again there may be new content
- window.setTimeout(main, 2000)
- }
- })()