// ==UserScript==
// @name Ratings on IMDb
// @name:de Bewertungen auf IMDb
// @name:es Calificaciones en IMDb
// @name:fr Évaluations sur IMDb
// @name:it Valutazioni su IMDb
// @name:ru Рейтинги на IMDb
// @name:zh-CN IMDb上的评分
// @author Davide <iFelix18@protonmail.com>
// @namespace https://github.com/iFelix18
// @icon https://www.google.com/s2/favicons?sz=32&domain=https://imdb.com
// @description Adds ratings from Rotten Tomatoes, Metacritic and MyAnimeList to IMDb
// @description:de Fügt Bewertungen von Rotten Tomatoes, Metacritic und MyAnimeList zu IMDb hinzu
// @description:es Agrega las calificaciones de Rotten Tomatoes, Metacritic y MyAnimeList a IMDb
// @description:fr Ajout des évaluations de Rotten Tomatoes, Metacritic et MyAnimeList à IMDb
// @description:it Aggiunge valutazioni da Rotten Tomatoes, Metacritic e MyAnimeList a IMDb
// @description:ru Добавляет рейтинги от Rotten Tomatoes, Metacritic и MyAnimeList на IMDb
// @description:zh-CN 将烂番茄、Metacritic和MyAnimeList的评级添加到IMDb中。
// @copyright 2021, Davide (https://github.com/iFelix18)
// @license MIT
// @version 3.0.4
// @homepage https://github.com/iFelix18/Userscripts#readme
// @homepageURL https://github.com/iFelix18/Userscripts#readme
// @supportURL https://github.com/iFelix18/Userscripts/issues
// @updateURL https://raw.githubusercontent.com/iFelix18/Userscripts/master/userscripts/meta/ratings-on-imdb.meta.js
// @downloadURL https://raw.githubusercontent.com/iFelix18/Userscripts/master/userscripts/ratings-on-imdb.user.js
// @require https://fastly.jsdelivr.net/gh/sizzlemctwizzle/GM_config@43fd0fe4de1166f343883511e53546e87840aeaf/gm_config.min.js
// @require https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.6.1/lib/index.min.js
// @require https://fastly.jsdelivr.net/npm/@ifelix18/omdb@4.1.3/lib/index.min.js
// @require https://fastly.jsdelivr.net/npm/@ifelix18/rottentomatoes@3.0.1/lib/index.min.js
// @require https://fastly.jsdelivr.net/npm/@ifelix18/jikan@3.0.0/lib/index.min.js
// @require https://fastly.jsdelivr.net/npm/@ifelix18/ratings@5.0.1/lib/index.min.js
// @require https://fastly.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js
// @require https://fastly.jsdelivr.net/npm/handlebars@4.7.7/dist/handlebars.min.js
// @require https://fastly.jsdelivr.net/npm/tooltipster@4.2.8/dist/js/tooltipster.bundle.min.js
// @require https://fastly.jsdelivr.net/npm/tooltipster-follower@0.1.5/dist/js/tooltipster-follower.min.js
// @resource tooltipster https://fastly.jsdelivr.net/npm/tooltipster@4.2.8/dist/css/tooltipster.bundle.min.css
// @resource follower https://fastly.jsdelivr.net/npm/tooltipster-follower@0.1.5/dist/css/tooltipster-follower.min.css
// @match *://www.imdb.com/*
// @connect api.jikan.moe
// @connect omdbapi.com
// @connect rottentomatoes.com
// @compatible chrome
// @compatible edge
// @compatible firefox
// @compatible safari
// @grant GM_getValue
// @grant GM_setValue
// @grant GM.deleteValue
// @grant GM.getValue
// @grant GM.listValues
// @grant GM.registerMenuCommand
// @grant GM.setValue
// @grant GM.xmlHttpRequest
// @run-at document-start
// @inject-into content
// ==/UserScript==
/* global $, GM_config, Handlebars, Ratings, UU */
(() => {
//* Constants
const id = GM.info.script.name.toLowerCase().replace(/\s/g, '-')
const title = `${GM.info.script.name} v${GM.info.script.version} Settings`
const fields = {
OMDbApiKey: {
label: 'OMDb API Key',
section: ['You can request a free OMDb API Key at:', 'https://www.omdbapi.com/apikey.aspx'],
type: 'text',
title: 'Your OMDb API Key',
size: 20,
default: ''
},
logging: {
label: 'Logging',
section: ['Develop'],
labelPos: 'right',
type: 'checkbox',
default: false
},
debugging: {
label: 'Debugging',
labelPos: 'right',
type: 'checkbox',
default: false
},
clearCache: {
label: 'Clear the cache',
type: 'button',
click: async () => {
const values = await GM.listValues()
for (const value of values) {
const cache = await GM.getValue(value) // get cache
if (cache.time) { GM.deleteValue(value) } // delete cache
}
UU.log('cache cleared')
GM_config.close()
}
}
}
//* GM_config
GM_config.init({
id,
title,
fields,
css: ':root{--accent:rgb(226, 182, 22);--background:rgb(24, 26, 27);--black:rgb(0, 0, 0);--font:Roboto,Helvetica,Arial,sans-serif;--white:rgb(255, 255, 255)}#ratings-on-imdb *{color:var(--white)!important;font-family:var(--font)!important;font-size:14px!important;font-weight:400!important}#ratings-on-imdb{background:var(--background)!important}#ratings-on-imdb .config_header{font-size:20pt!important;line-height:1.1!important}#ratings-on-imdb .section_desc,#ratings-on-imdb .section_header{background-color:var(--accent)!important;border:1px solid transparent!important;color:var(--black)!important}#ratings-on-imdb .config_var{align-items:center!important;display:flex!important}#ratings-on-imdb_field_OMDbApiKey{background-color:var(--white)!important;border:1px solid var(--black)!important;color:var(--black)!important;flex:1!important}#ratings-on-imdb input[type=checkbox]{appearance:none!important;background-color:var(--white)!important;border:1px solid var(--black)!important;cursor:pointer!important;height:15px!important;width:15px!important}#ratings-on-imdb input[type=checkbox]:checked{appearance:checkbox!important;background-color:var(--accent)!important}#ratings-on-imdb button,#ratings-on-imdb input[type=button]{background:var(--white)!important;border:1px solid var(--black)!important;color:var(--black)!important}#ratings-on-imdb button:hover,#ratings-on-imdb input[type=button]:hover{filter:brightness(85%)!important}#ratings-on-imdb .reset{margin-right:10px!important}',
events: {
init: () => {
// initial configuration if OMDb API Key is missing
if (!GM_config.isOpen && GM_config.get('OMDbApiKey') === '') {
$(() => GM_config.open())
}
//! Userscripts Safari: GM.registerMenuCommand is missing
if (GM.info.scriptHandler !== 'Userscripts') GM.registerMenuCommand('Configure', () => GM_config.open())
},
save: () => {
if (GM_config.isOpen) {
if (GM_config.get('OMDbApiKey') === '') {
UU.alert('check your settings and save')
} else {
UU.alert('settings saved')
GM_config.close()
setTimeout(window.location.reload(false), 500)
}
}
}
}
})
//* Utils
UU.init({ id, logging: GM_config.get('logging') })
//* Ratings
const rating = new Ratings({
omdb: {
api_key: GM_config.get('OMDbApiKey')
},
debug: GM_config.get('debugging'),
cache: {
active: true,
TTL: 3600
}
})
//* Handlebars
Handlebars.registerHelper('ifEqual', function (a, b, options) {
if (a === b) return options.fn(this)
return options.inverse(this)
})
//* Functions
/**
* Adds various resources to the head
*/
const addResources = () => {
// add tooltipster resources
UU.resource.add('tooltipster', 'stylesheet')
UU.resource.add('follower', 'stylesheet')
// add tooltipster customized style
UU.addStyle('.tooltipster-follower.tooltipster-light.tooltipster-light-customized { height: fit-content !important; font-family: \'Source Sans Pro\', Arial, sans-serif !important; } .tooltipster-follower.tooltipster-light.tooltipster-light-customized .tooltipster-box { border-radius: 8px !important; } .tooltipster-follower.tooltipster-light.tooltipster-light-customized .tooltipster-content { padding: 8px 20px !important; padding-right: 0px !important; display: inline-flex !important; align-items: center !important; }')
}
/**
* Adds a link to the menu to access the script configuration
*/
const addSettings = () => {
const menu = `<div class=${id} style=order:5;white-space:nowrap;font:inherit><a href=""onclick=return!1 style=text-decoration:none;font-weight:bolder>${GM.info.script.name}</a></div>`
$('.navbar__inner > .navbar__imdbpro').after(menu)
$(`.${id}`).click(() => GM_config.open())
}
/**
* Clear old data from the cache
*/
const clearOldCache = async () => {
const values = await GM.listValues()
for (const value of values) {
const cache = await GM.getValue(value) // get cache
if ((Date.now() - cache.time) > 3_600_000) { GM.deleteValue(value) } // delete old cache
}
}
/**
* Get item data
*
* @param {*} item Item
* @returns {object} Data
*/
const getData = (item) => {
switch (typeof item) {
case 'undefined': {
const id = $('meta[property="imdb:pageConst"]').first().attr('content')
return { id }
}
case 'object': {
const data = $(item).find('a[href^="/title/"]').first().attr('href')
const id = /^(?:\/title\/)?(tt\d*)/gm.exec(data)
return { id: id[1] }
}
default:
break
}
}
/**
* Add template
*
* @param {object} target HTML target for template
*/
const addTemplate = (target) => {
/* cspell: disable-next-line */
const template = '<div style=display:inline-flex class=external-ratings></div><script id=external-ratings-template type=text/x-handlebars-template>{{#each ratings}} {{#ifEqual this.rating "N/A"}} {{else}}<div style=display:inline-flex;flex-direction:column;-moz-box-align:center;align-items:center;padding:.25rem;margin-right:.5rem class="rating-bar__base-button {{this.source}}-rating"><div style=text-transform:uppercase;font-family:Roboto,Helvetica,Arial,sans-serif;font-size:.75rem;font-weight:600;letter-spacing:.16667em;line-height:1rem;color:rgba(255,255,255,.7);white-space:nowrap;margin-bottom:.25rem class=external-rating-title>{{this.source}}</div><a class="external-rating-link ipc-button ipc-button--core-baseAlt ipc-button--on-textPrimary ipc-text-button" href={{this.url}}><div class=ipc-button__text><div style=display:flex;-moz-box-align:center;align-items:center><div style=display:flex;-moz-box-align:center;align-items:center;height:2rem;width:2rem;margin-right:.25rem class=external-rating-logo><img alt=logo src={{this.logo}} width=24></div><div style=display:inline-flex;flex-direction:column;align-items:flex-start;padding-right:.25rem><div style=display:flex;-moz-box-align:center;align-items:center;font-family:Roboto,Helvetica,Arial,sans-serif;font-size:1rem;font-weight:400;letter-spacing:.03125em;text-transform:none;color:rgba(255,255,255,.7);line-height:1.5rem;margin-bottom:-.125rem><span class=external-rating-vote style=font-family:Roboto,Helvetica,Arial,sans-serif;font-size:1.25rem;font-weight:600;letter-spacing:.0125em;line-height:1.5rem;text-transform:none;color:#fff;padding-right:.125rem>{{this.rating}} </span>{{#ifEqual this.rating "N/A"}} {{else}} <span class=external-rating-symbol>{{this.symbol}} </span>{{/ifEqual}}</div>{{#ifEqual this.rating "N/A"}} {{else}} {{#ifEqual this.source "metascore"}}<div style="font-family:Roboto,Helvetica,Arial,sans-serif;font-size:.75rem;font-weight:400;letter-spacing:.03333em;line-height:1rem;text-transform:none;color:transparent;width:100%;background:linear-gradient(to top,transparent 0,transparent 25%,{{this.votes}} 25%,{{this.votes}} 75%,transparent 75%,transparent 100%)" class=external-rating-votes>{{this.rating}}</div>{{else}}<div style=font-family:Roboto,Helvetica,Arial,sans-serif;font-size:.75rem;font-weight:400;letter-spacing:.03333em;line-height:1rem;text-transform:none;color:rgba(255,255,255,.7) class=external-rating-votes>{{this.votes}}</div>{{/ifEqual}} {{/ifEqual}}</div></div></div></a></div>{{/ifEqual}} {{/each}}</script>'
$(template).insertAfter(target)
}
//* Script
$(async () => {
addResources() // add resources
addSettings() // add settings
clearOldCache() // clear old cache
// check if it is on the main page
const target = $('section > div:nth-child(2) .rating-bar__base-button:nth-child(1)')
if (target.length === 0) return
// get item data
const data = getData()
const id = data.id // TMDb ID
if (!id) return // check if an ID exists
UU.log(`ID is '${id}'`)
// get ratings
const ratings = await rating.get({ id }).then().catch(error => UU.error(error))
const elaboratedRatings = await rating.elaborate(ratings).then().catch(error => UU.error(error))
for (const key of Object.keys(elaboratedRatings).filter(key => new Set(['imdb']).has(key))) { // delete IMDb from data
delete elaboratedRatings[key]
}
// add template
addTemplate(target)
// compile template
const template = Handlebars.compile($('#external-ratings-template').html())
$('.external-ratings').html(template({ ratings: elaboratedRatings }))
})
UU.observe.creation('.ipc-poster-card.ipc-poster-card--dynamic-width', (item) => { // tooltipster
$(item).tooltipster({
content: '<span class=loading style=padding-right:20px>Loading...</span>',
contentAsHTML: true,
plugins: ['follower'],
theme: ['tooltipster-light', 'tooltipster-light-customized'],
updateAnimation: false,
functionBefore: async (instance, helper) => {
const $origin = $(helper.origin)
$(item).find('*').contents().removeAttr('title') // ? fix double tooltip
// get item data
const data = getData(item)
const id = data.id // TMDb ID
if (!id) return // check if an ID exists
UU.log(`ID is '${id}'`)
// tooltipster data has not been loaded yet
if ($origin.data('loaded') !== true) {
// get ratings
const ratings = await rating.get({ id }).then().catch(error => UU.error(error))
const elaboratedRatings = await rating.elaborate(ratings).then().catch(error => UU.error(error))
for (const key of Object.keys(elaboratedRatings)) { // delete empty from data
if (elaboratedRatings[key].rating === 'N/A') delete elaboratedRatings[key]
}
// compile template
if ($.isEmptyObject(elaboratedRatings)) { // ratings unavailable
instance.content('<span class=ratings-unavailable style=padding-right:20px>Ratings not available</span>')
} else {
const template = Handlebars.compile('{{#each ratings}}<div class=external-rating><div class={{this.source}}-rating style=display:inline-flex;align-items:center;margin-right:20px><div class=logo style=display:inline-flex><img alt=logo src={{this.logo}} width=46></div><div class=text style=font-weight:700;margin-left:6px;display:inline-flex;flex-direction:column;align-items:flex-start><div class=vote style=display:flex;align-items:center;align-content:center;justify-content:center>{{this.rating}} <span style=font-weight:400;font-size:80%;opacity:.8>{{this.symbol}}</span></div>{{#ifEqual this.votes "N/A"}} {{else}} {{#ifEqual this.source "metascore"}}<div class=votes style="display:flex;align-items:center;align-content:center;justify-content:center;background:linear-gradient(to top,transparent 0,transparent 25%,{{this.votes}} 25%,{{this.votes}} 75%,transparent 75%,transparent 100%);color:transparent">{{this.rating}}</div>{{else}}<div class=votes style=font-weight:400;opacity:.8>{{this.votes}}</div>{{/ifEqual}} {{/ifEqual}}</div></div></div>{{/each}}')
instance.content(template({ ratings: elaboratedRatings }))
}
// now tooltipster data has been loaded
$origin.data('loaded', true)
}
}
})
})
})()
Donate for the site OpenUserJS
Are you sure you want to go to an external site to donate a monetary value?
WARNING: Some countries laws may supersede the payment processors policy such as the GDPR and PayPal. While it is highly appreciated to donate, please check with your countries privacy and identity laws regarding privacy of information first. Use at your utmost discretion.