NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name PTP Movie Awards
// @namespace https://openuserjs.org/users/SB100
// @description Shows the awards a movie has won on movie pages
// @version 2.0.0
// @author SB100
// @copyright 2024, SB100 (https://openuserjs.org/users/SB100)
// @updateURL https://openuserjs.org/meta/SB100/PTP_Movie_Awards.meta.js
// @license MIT
// @match https://*passthepopcorn.me/torrents.php?*id=*
// @grant GM.xmlHttpRequest
// @connect imdb.com
// ==/UserScript==
// ==OpenUserJS==
// @author SB100
// ==/OpenUserJS==
/**
* =============================
* ADVANCED OPTIONS
* =============================
*/
// Which awards to show on a movie page. Copy headers exactly as is from the IMDb awards page
// e.g. https://www.imdb.com/title/tt10272386/awards/
const SETTING_AWARDS = [
'Academy Awards, USA',
'Golden Globes, USA',
'BAFTA Awards',
];
/**
* Force the award panel to always start in a specific state (opened or closed). Otherwise, the toggle state will be remembered, and used on other movie pages.
* Allowed enums: 'none', 'open', 'close'
*/
const SETTING_FORCE_PANEL_DEFAULT_STATE = 'none';
/**
* Choose whether the award name should be aligned to the left or right in the table. Default is right
*/
const SETTING_ALIGN_AWARD_NAME = 'right';
/**
* Output console.log lines to the console
*/
const DEBUG_ENABLED = true;
/**
* =============================
* END ADVANCED OPTIONS
* DO NOT MODIFY BELOW THIS LINE
* =============================
*/
const CONFIG_IMDB_AWARDS_URL = 'https://www.imdb.com/title/{imdbId}/awards';
/**
* Show console.log lines if debug is enabled
*/
function debug(...str) {
if (!DEBUG_ENABLED) {
return;
}
// eslint-disable-next-line no-console
console.log(...str);
}
/**
* Insert a new node after an existing node
*/
function insertAfter(newNode, existingNode) {
existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
}
/**
* Try parsing a string into JSON, otherwise fallback
*/
function JsonParseWithDefault(s, fallback = null) {
try {
return JSON.parse(s);
}
catch (e) {
return fallback;
}
}
/**
* Turn a HTML string into a HTML element so that we can run querySelector calls against it
*/
function htmlToElement(html) {
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content;
}
/**
* Get all settings stored in localStorage for this script
*/
function getSettings() {
const settings = window.localStorage.getItem('awardSettings');
return JsonParseWithDefault(settings || {}, {});
}
/**
* Set a setting into localStorage for this script
*/
function setSetting(name, value) {
const json = getSettings();
json[name] = value;
window.localStorage.setItem('awardSettings', JSON.stringify(json));
}
/**
* Get the default open state of the awards panel
*/
function getInitialOpenState() {
if (SETTING_FORCE_PANEL_DEFAULT_STATE === 'open') return true;
if (SETTING_FORCE_PANEL_DEFAULT_STATE === 'close') return false;
const settings = getSettings();
return !!settings.toggleOpen;
}
/**
* Find the movies IMDb ID from the page
*/
function findImdbIdFromPage() {
const elem = document.getElementById('imdb-title-link');
const href = elem && elem.href;
return href && href.match(/tt\d+/)[0];
}
/**
* Query the IMDb awards page for any results a movie might have
*/
function queryAwardsPage(imdbId) {
let resolver;
let rejecter;
const p = new Promise((resolveFn, rejectFn) => {
resolver = resolveFn;
rejecter = rejectFn;
});
const url = CONFIG_IMDB_AWARDS_URL.replace('{imdbId}', imdbId);
GM.xmlHttpRequest({
method: 'get',
url,
timeout: 10000,
onloadstart: () => {},
onload: (result) => {
if (result.status !== 200) {
debug('[PTP Movie Awards]', result);
rejecter(new Error('Error received from IMDB'));
return;
}
resolver(htmlToElement(result.response));
},
onerror: (result) => {
rejecter(result);
},
ontimeout: (result) => {
rejecter(result);
},
});
return p;
}
/**
* Process the IMDb page into JSON
*/
function processResults(page) {
try {
const data = JsonParseWithDefault(
page.querySelector('#__NEXT_DATA__')?.innerText, {
props: {
pageProps: {
contentData: {
categories: [],
},
},
},
}
);
const wanted = data.props.pageProps.contentData.categories
.filter((info) => SETTING_AWARDS.includes(info.name))
.map((info) => {
const winnerAndNomineeInfo = info.section.items.reduce((acc, item) => {
if (!Array.isArray(acc[item.rowTitle])) {
acc[item.rowTitle] = [];
}
acc[item.rowTitle].push({
catName: item.listContent[0].text,
people: item.subListContent.map((content) => content.text),
});
return acc;
}, {});
return {
name: info.name,
data: winnerAndNomineeInfo,
};
});
const summary = page
.querySelector('[data-testid="awards-signpost"]')
?.innerText?.toLowerCase();
return {
total: data.props.pageProps.contentData.categories.length,
summary,
wanted,
};
}
catch (e) {
debug('[PTP Movie Awards]', '[Process Error]', e.message);
return [];
}
}
/**
* Create the main table that holds all of the award data
*/
function createTable(results, summary, total) {
const rows = (info) =>
info
.map(
(obj) => `
<tr>
<td class="award__description-left ${
SETTING_ALIGN_AWARD_NAME === 'right'
? ''
: 'award__description-left--aligned-left'
} award__description--overflow" title="${obj.catName}">
<span class="award__description--dimmed">${obj.catName}</span>
</td>
<td class="award__description-right award__description--overflow" title="${obj.people.join(
', '
)}">${obj.people.join(', ')}</td>
</tr>
`
)
.join('');
const table = document.createElement('table');
table.classList.add('table', 'table--bordered');
const html = results
.map(
({
name,
data
}) => `<tr>
<th colspan="2" class="award__name">${name}</th>
${Object.entries(data)
.map(
([title, info]) => `
<tr><td colspan="2" class="award__subheader">${title}</td></tr>
${rows(info)}
`
)
.join('')}
</tr>`
)
.join('');
if (html === '') {
table.innerHTML = `<tr><td colspan="2">${
total > 0 ? `${total} results filtered out` : 'None'
}</td></tr>`;
return table;
}
table.innerHTML = `<tr><th colspan="2" class="award--center">${summary}</th></tr>${html}`;
return table;
}
/**
* Create the main panel body. If it is open, fill it with the award data table
*/
async function createPanelBody(panel) {
if (panel.dataset.hasRun === '1') {
return;
}
const body = panel.querySelector('.panel__body');
if (!body) {
debug('[PTP Movie Awards]', 'Could not find panel body');
}
body.innerHTML = `<table class='table table--bordered'><tr><td colspan="2">Loading …</td></tr></table>`;
try {
const imdbId = findImdbIdFromPage();
const results = await queryAwardsPage(imdbId);
const {
total,
summary,
wanted
} = processResults(results);
const table = createTable(wanted, summary, total);
body.innerHTML = '';
// scroll and fade element. The class will be applied if the table is > 300px below
const maxHeightContainer = document.createElement('div');
maxHeightContainer.appendChild(table);
body.appendChild(maxHeightContainer);
if (table.offsetHeight >= 300) {
maxHeightContainer.classList.add('award__fade-out');
}
}
catch (e) {
body.innerHTML = `<table class='table table--bordered'><tr><td colspan="2">Error: ${e.message}</td></tr></table>`;
return;
}
// eslint-disable-next-line no-param-reassign
panel.dataset.hasRun = 1;
}
/**
* Create the main panel
*/
function createPanel() {
const isOpen = getInitialOpenState();
const panel = document.createElement('div');
panel.id = 'panel__awards';
panel.classList.add('panel');
panel.innerHTML = `<div class="panel__heading">
<span class="panel__heading__title">Awards</span>
<a id="panel__awards-toggle" class="panel__heading__toggler" style="font-size: 0.9em;" title="Toggle" href="#awards">(${
isOpen ? 'Hide' : 'Show'
} awards)</a>
</div>
<div class="panel__body ${isOpen ? '' : 'hidden'}"></div>`;
const castTable = document.querySelector(
'.main-column > table.table:not(.torrent_table):not(#requests)'
);
if (!castTable) {
debug('[PTP Movie Awards]', 'Could not find cast table');
return;
}
insertAfter(panel, castTable);
if (isOpen) {
createPanelBody(panel);
}
const panelToggle = document.querySelector('#panel__awards-toggle');
panelToggle.addEventListener('click', () => {
const panelBody = document.querySelector('#panel__awards .panel__body');
if (panelBody.classList.contains('hidden')) {
panelBody.classList.remove('hidden');
panelToggle.innerText = '(Hide awards)';
setSetting('toggleOpen', true);
createPanelBody(panel);
}
else {
panelBody.classList.add('hidden');
panelToggle.innerText = '(Show awards)';
setSetting('toggleOpen', false);
}
});
}
/**
* All the custom styling that powers the script
*/
function createStyleTag() {
const css = `.award__fade-out {
display: block;
max-height: 300px;
padding-bottom: 15px;
overflow: hidden;
-webkit-mask-image: linear-gradient(to bottom, black 90%, transparent 100%);
mask-image: linear-gradient(to bottom, black 90%, transparent 100%);
}
.award__fade-out:hover {
overflow-y: scroll;
}
#panel__awards .panel__body {
padding:0;
}
.award--center {
text-align: center;
}
.award__name {
border-top: 1.5px dashed #CCC!important;
}
.award__description-left {
width: 50%;
text-align: right;
border-right: solid thin #555;
}
.award__description-left--aligned-left {
text-align: left;
}
.award__description-right {
width: 50%;
}
.award__description--overflow {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 0;
}
.award__subheader {
font-weight: bold;
}
.award__description--dimmed {
filter: brightness(80%);
}`;
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
// Main script runner
(function main() {
createStyleTag();
createPanel();
})();