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(); })();