NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @namespace https://openuserjs.org/users/SB100 // @name PTP Movie Awards // @description Shows the awards a movie has won on movie pages // @updateURL https://openuserjs.org/meta/SB100/PTP_Movie_Awards.meta.js // @version 1.3.1 // @author SB100 // @copyright 2021, SB100 (https://openuserjs.org/users/SB100) // @license MIT // @grant GM_xmlhttpRequest // @include https://passthepopcorn.me/torrents.php?id=* // @connect imdb.com // ==/UserScript== // ==OpenUserJS== // @author SB100 // ==/OpenUserJS== /* jshint esversion: 6 */ /** * ============================= * ADVANCED OPTIONS * ============================= */ // Which winning 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_TO_SHOW_WINNERS = [ 'Academy Awards, USA', 'Golden Globes, USA', 'BAFTA Awards', ]; // Which nominated 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_TO_SHOW_NOMINATED = [ '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'; /** * ============================= * END ADVANCED OPTIONS * DO NOT MODIFY BELOW THIS LINE * ============================= */ const CONFIG_IMDB_AWARDS_URL = 'https://www.imdb.com/title/{imdbId}/awards' /** * 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) { var template = document.createElement('template'); html = html.trim(); template.innerHTML = html; 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 ? true : false; } /** * 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: url, timeout: 10000, onloadstart: () => {}, onload: (result) => { if (result.status !== 200) { console.log('[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; } /** * Get the summary header from the page "Showing all 5 wins and 10 nominations" */ function getSummary(page) { return page.querySelector('.header .nav .desc')?.innerText?.replace('Showing all ', ''); } /** * Process the IMDb page into JSON */ function processResults(page) { const elems = Array.from(page.querySelectorAll('.article .header + h3, .article br + h3, .article h3 + .awards')); let lastTitle = null; return elems.reduce((result, elem) => { if (elem.nodeName === 'H3') { // match the name and the year from the h3 title const matches = elem.innerText.replace(/[\r\n]+/g, '').match(/([A-Za-zÀ-ÖØ-öø-ÿ\s\.',:;\(\)]+?)([\d]{4,4})/i); if (!matches) { // if there are no matches, we can't process the following table, so set lastTitle to null (will cause next table to be skipped) lastTitle = null; return result; } const sanitizedName = matches[1].trim(); lastTitle = sanitizedName; const year = parseInt(matches[2], 10); const link = elem.innerHTML.match(/\"(\/event(?:[^\?]+))/i)?.[1]; // create an object holding the year info. We'll populate nominee and winner data in the next if block if (Object.prototype.hasOwnProperty.call(result, sanitizedName) === false) { result[sanitizedName] = { year, link, winner: [], nominee: [] }; } } else if (elem.nodeName === 'TABLE') { // make sure we processed a title just before this if (lastTitle === null) { return result; } // we'll be looping through rows and processing cells. This var will hold the row grouping so we can add each cell's results to it let lastOutcome = null; // sometimes the award type doesn't have a header in the second column. In which case, this var will hold the title from the first as a backup let lastAwardCategory = null; const tds = Array.from(elem.querySelectorAll('td')); for (let i = 0, len = tds.length; i < len; i += 1) { const td = tds[i]; if (td.classList.contains('title_award_outcome')) { // save the outcome name to a variable so we can populate it with the results from the next loop lastOutcome = td.querySelector('b').innerText.toLowerCase().trim(); // save the award category as a backup incase there is no description in the next column lastAwardCategory = td.querySelector('.award_category')?.innerText?.trim(); // create an empty award and (winner/nominee) array result[lastTitle][lastOutcome] = []; } else if (lastOutcome !== null) { // if some note exists in the cell, remove it. const notes = td.querySelector('.award_detail_notes'); notes && notes.remove(); // make sure we have a name. If so, the follow cells contain the award description and people // match everything before the first html element const descriptionMatch = td.innerHTML.match(/^([^\<]+)/i); // match the people for this specific description // match all <a> tags as all names are hyperlinked const people = [...td.innerHTML.matchAll(/<a(?:[^\>]+)>([^\<]+)/ig)].map(matches => matches[1]); // add to result result[lastTitle][lastOutcome].push({ description: descriptionMatch[1].trim() || lastAwardCategory, people }); } } } return result; }, {}); } /** * Filter the awards down to only the ones we want to see, as defined in the settings */ function filterResults(results) { const uniqueAwards = [...new Set([...SETTING_AWARDS_TO_SHOW_WINNERS, ...SETTING_AWARDS_TO_SHOW_NOMINATED])]; return Object.entries(results).reduce((result, [name, obj]) => { // remove entries we're not interested in if (uniqueAwards.includes(name) === false) { return result; } // remove winners we're not interested in if (SETTING_AWARDS_TO_SHOW_WINNERS.includes(name) === false) obj.winner = []; // remove nominees we're not interested in if (SETTING_AWARDS_TO_SHOW_NOMINATED.includes(name) === false) obj.nominee = []; // add to new result and return result[name] = obj; return result; }, {}); } /** * Create the main table that holds all of the award data */ function createTable(results, summary) { const rows = (section) => section.map(obj => `<tr> <td class="award__description-left ${SETTING_ALIGN_AWARD_NAME === 'right' ? '' : 'award__description-left--aligned-left'} award__description--overflow" title="${obj.description}"> <span class="award__description--dimmed">${obj.description}</span> </td> <td class="award__description-right award__description--overflow" title="${obj.people.join(', ')}">${obj.people.join(', ')}</td> </tr>`).join(''); // build a fake summary object so that we can use the rows function above. const fakeSummaryObj = [{ description: 'Summary', people: [summary] }] const table = document.createElement('table'); table.classList.add('table', 'table--bordered'); let html = summary ? rows(fakeSummaryObj) : ''; Object.entries(results).forEach(([name, obj], idx) => { const hasWinner = obj.winner.length > 0; const hasNominee = obj.nominee.length > 0; const hasEither = hasWinner || hasNominee; if (!hasEither) { return; } html += `<tr> <th colspan="2" ${idx !== 0 ? 'class="award__name"' : ''}> ${name} <a href="${obj.link ? `https://www.imdb.com${obj.link}` : '#unknown'}" class="award__year" target="_blank" rel="noopener noreferrer">${obj.year}</a> </th> </tr> ${hasWinner ? `<tr><td colspan="2" class="award__subheader">Winner</td></tr>` : ''} ${hasWinner ? rows(obj.winner) : ''} ${hasNominee ? `<tr><td colspan="2" class="award__subheader">Nominated</td></tr>` : ''} ${hasNominee ? rows(obj.nominee) : ''}`; }); if (html === '') { html += '<tr><td colspan="2">None</td></tr>' } table.innerHTML = 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) { console.log('[PTP Movie Awards]', 'Could not find panel body'); } body.innerHTML = `<table class='table table--bordered'><tr><td colspan="2">Loading …</td></tr></table>`; const imdbId = findImdbIdFromPage(); const results = await queryAwardsPage(imdbId); const processed = processResults(results); const filtered = filterResults(processed); const summary = getSummary(results); try { const table = createTable(filtered, summary); 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; } 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) { console.log('[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__name { border-top: 1px solid #CCC!important; } .award__year { font-size: 0.8em; font-weight: normal; padding-left: 5px; } .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 () { 'use strict'; createStyleTag(); createPanel(); })();