// ==UserScript==
// @name Power IMDB:Trakt.tv & YouTube Trailers (Powered by PowerSheet.ai - Free No-Code Creation Platform)
// @author PowerSheet.ai – Empower the Future of Work | The free 1st intelligent universal #1 no-code/low-code platform | Collaborative creation, modeling, planning, analytics & web extensions | Sign up now free @ https://PowerSheet.ai !
// @copyright © 2021 PowerSheet.ai – Empower the Future of Work | The free 1st intelligent universal #1 no-code/low-code platform | Collaborative creation, modeling, planning, analytics & web extensions | Sign up now free @ https://PowerSheet.ai !
// @description "YouTube Trailer" & "View on Trakt.tv" on IMDB navbar for TV & movie rating, watchlist & progress. — Powered by👉🆓 PowerSheet.ai | The free 1st intelligent universal no-code/low-code platform | Collaborative creation, modeling, planning, analytics & web extensions | Sign up now free @ https://PowerSheet.ai
// @version 2.1.0
// @include /^https?://(\w+\.)imdb\.com/title//
// @namespace https://PowerSheet.ai
// @homepageURL https://PowerSheet.ai/Free-NoCode-App-BI-Excel-Blockchain
// @supportURL https://PowerSheet.ai/Free-NoCode-App-BI-Excel-Blockchain
// @contributionURL https://PowerSheet.ai/Free-NoCode-App-BI-Excel-Blockchain
// @updateUrl https://greasyfork.org/scripts/402912-power-imdb-trakt-tv-links-powered-by-powersheet-ai-no-code-app-bi-bot-webext-blockchain-platform
// @license CC-BY-NC-SA-4.0
// Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (https://creativecommons.org/licenses/by-nc-sa/4.0/)
// @grant GM_getValue
// @grant GM_setValue
// @inject-into content
// @run-at document-end
// @priority 1
// @icon https://static.wixstatic.com/media/4d2eb7_adf8916f24a74cc3934c22130df50a0f%7Emv2.png/v1/fill/w_32%2Ch_32%2Clg_1%2Cusm_0.66_1.00_0.01/4d2eb7_adf8916f24a74cc3934c22130df50a0f%7Emv2.png
// @screenshot https://greasyfork.org/system/screenshots/screenshots/000/021/114/original/Power-IMDB-Screenshot.jpg?1590285123
// ==/UserScript==
// User Script metadata docs: https://greasyfork.org/en/help/meta-keys (Name limited to 100 char, Description to 500)
// NOTE: Adds "View on Trakt.tv" button with direct link for each movie, TV show and video on IMDB.com so you can browse ratings & reviews, add to watchlists and track what you've watched on Trakt.
// ● Powered by the FREE PowerSheet.ai – the free all-in-one no-code remote collaboration platform for automatic mobile apps, analytics, planning, bots, automation, blockchain databases – in Excel, Microsoft teams, Web Extensions, decentralized PWA, embedded anywhere
// ● Auto-create, collaborate on, sync, automate, publish and embed your own no-code Web 4.0 & mobile smart apps, browser extensions, bots, BI dashboards, spreadsheets, plans, RPA, data connectors and embedded realtime blockchain databases.
// ● Remotely collaborative with easy AI powered analytics, planning, DApp creation, remote work management, assignments, web scraping, RPA, data prep, intelligent automation and realtime remote collaboration with Power Sheet.
// ● Instantly auto publish everywhere as mobile, Web 4.0, Excel, decentralized PWA, desktop, offline and embedded apps.
// ● Collaboratively create and embed in Excel, Microsoft Teams, PowerPoint, SharePoint, web browsers, websites, existing apps and anywhere.
// ● Simultaneously sell and share everywhere with our universal marketplace for smart apps, templates, connectors and content.
// ● Free for unlimited users. No coding, install, server or IT setup required.
// ● Sign up 🆓 @ 👉 https://PowerSheet.ai.
(async function () {
//constants:
const useSettings = true;
const awaitSavingDefaults = false;
const debugRegex = false;
const scriptLogPrefix = '[Power IMDB (User Script)] ';
//MAYBE: Add setting to include year in YouTube search (but outside of quotes)?
const traktSearchLink = 'https://trakt.tv/search/imdb?q=';
const youTubeTrailerSearchLinkBase = 'https://www.youtube.com/results?search_query=';
const youTubeTrailerSearchLinkSuffix = '+trailer';
const buttonPadding = '.3rem'; //reduced padding vs. original 1rem for Watchlist etc buttons
const labelIconPadding = '4px'; //reduced padding vs. original 1rem for Watchlist etc buttons
let buttonOrderPos = 5; //ensure it's the last button. auto-incremented //8 = after User drop-down, 7 or 6 = after Watchlist, 5 = before Watchlist
let hideLabels = false;
try {
//load user settings and state:
hideLabels = await loadBoolSetting('hide-labels', false);
//image constants:
const traktIconSvg = `<svg id="open-trakt-icon" width="24" height="24" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="-334.1 223.1 347 347" xml:space="preserve">
<style>.st1{fill:#ed2224}</style><circle cx="-160.6" cy="396.6" r="162.5" fill="#fff"></circle><path class="st1" d="M-256.9 485c23.8 26 58.1 42.2 96.3 42.2 19.5 0 37.9-4.3 54.5-11.9l-90.7-90.5-60.1 60.2z"></path><path class="st1" d="M-197.2 370.1l-68.7 68.5-9.2-9.2 72.3-72.3 84.4-84.4c-13.2-4.5-27.4-7-42.2-7-72.3 0-130.9 58.6-130.9 130.9 0 29.4 9.7 56.6 26.3 78.6l68.5-68.5 4.7 4.5 98.1 98.1c2-1.1 3.8-2.2 5.6-3.6l-108.4-108.4-65.8 65.8-9.2-9.2 75-75 4.7 4.5 114.5 114.2c1.8-1.3 3.4-2.9 4.9-4.3L-196 369.9l-1.2.2z"></path><path d="M-63.4 484.1c20.9-23.1 33.7-53.9 33.7-87.5 0-52.5-31-97.6-75.4-118.5l-82.4 82.1 124.1 123.9zM-155.9 384l-9.2-9.2 64.9-64.9 9.2 9.2-64.9 64.9zm61.5-89.1l-74.7 74.7-9.2-9.2 74.7-74.7 9.2 9.2z" fill="#ed1c24"></path><path class="st1" d="M-160.6 559.1c-89.6 0-162.5-72.9-162.5-162.5s72.9-162.5 162.5-162.5S1.9 307 1.9 396.6-71 559.1-160.6 559.1zm0-308.6c-80.6 0-146.1 65.5-146.1 146.1s65.5 146.1 146.1 146.1 146.1-65.5 146.1-146.1S-80 250.5-160.6 250.5z"></path>
</svg>`;
//From: https://developers.google.com/site-assets/logo-youtube.svg
const youTubeIconSvg = `<svg id="open-youtube-trailer-icon" width="24" height="24" version= xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 158 110" enable-background="new 0 0 158 110" xml:space="preserve">
<path fill="#f00" d="M154.4,17.5c-1.8-6.7-7.1-12-13.9-13.8C128.2,0.5,79,0.5,79,0.5s-48.3-0.2-60.6,3 c-6.8,1.8-13.3,7.3-15.1,14C0,29.7,0.3,55,0.3,55S0,80.3,3.3,92.5c1.8,6.7,8.4,12.2,15.1,14c12.3,3.3,60.6,3,60.6,3s48.3,0.2,60.6-3 c6.8-1.8,13.1-7.3,14.9-14c3.3-12.1,3.3-37.5,3.3-37.5S157.7,29.7,154.4,17.5z"/>
<polygon fill="#fff" points="63.9,79.2 103.2,55 63.9,30.8 "/>
</svg>`;
//generate HTML for buttons
const trailerButton = buttonHtml('Trailer', //label
getYouTubeTrailerSearchLink(), //url
'Open YouTube trailer video search in new tab', //tooltip
'open-youtube-trailer',
youTubeIconSvg
);
const traktButton = buttonHtml('Trakt', //label
getTraktTitleUrl(), //url
'Open Trakt.tv page for Title', //tooltip
'open-trakt',
traktIconSvg
);
const linkHtml = trailerButton + traktButton;
//NOTE: Delay required for IMDB 2021 redesign preview
await sleep(1000);
//add button HTML to navbar
addButtonsToNavBar(linkHtml);
} catch(e) {
console.error(scriptLogPrefix + `Error creating or adding buttons or loading settings: `, e);
}
//Functions:
//returns html defining button for the navbar
function buttonHtml(label, linkUrl, tooltip, idBase, iconSvg) {
//return empty if no URL defined, to skip this button
if (!linkUrl) return '';
return `
<div id+"${idBase}-div" style="
order: ${buttonOrderPos};
">
<a id="${idBase}-link" title="${tooltip}" target="_blank" href="${linkUrl}" tabindex="0" class="ipc-button ipc-button--single-padding ipc-button--default-height ipc-button--core-baseAlt ipc-button--theme-baseAlt ipc-button--on-textPrimary ipc-text-button" style="
padding: 0 ${buttonPadding};
">
${iconSvg}
${hideLabels ? '' : `
<div class="${idBase}-text ipc-button__text" style="
padding-left: ${labelIconPadding};
">${label}</div>`
}
</a>
</div>
`;
}
// sleep time expects milliseconds
async function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
function addButtonsToNavBar(buttonsHtml) {
if (!buttonsHtml) return;
//NOTE: Where inserted doesn't matter for button order, only changing buttonOrderPos does.
//insert as last child of navbar
let topNav = document.getElementsByClassName('ipc-page-content-container')[0]; //OR: 'navbar__user' or 'imdb-header__watchlist-button'
if (!topNav) {
console.error('Could not find navbar to add "View on Trakt.tv" and related buttons to, so adding to bottom of the page instead. May need to update CSS selector due to breaking IMDB changes.');
topNav = document.body || document.documentElement;
//Fallback to adding to top or bottom of page?
}
topNav.insertAdjacentHTML('beforeEnd', buttonsHtml); //OR: beforebegin or afterend if insert before/after existing nav button (though order doesn't matter)\
}
function getTraktTitleUrl() {
const imdbID = getImdbId();
return imdbID && (traktSearchLink + imdbID) || '';
}
function getYouTubeTrailerSearchLink() {
let title = getTitle();
if (!title) return '';
title = title.replace('"','');
if (!title) return '';
title = '"' + title + '"';
const titleEncoded = escapeForUrl(title);
//MAYBE: Setting to enable adding year (if parsed) outside of quotes?
return youTubeTrailerSearchLinkBase + titleEncoded + youTubeTrailerSearchLinkSuffix;
}
//parse title of TV series or movie (even from episode, etc. page) from tab title
function getTitle() {
const titleMeta = document.querySelector("meta[property='og:title']");
const tabTitle = titleMeta && titleMeta.getAttribute('content');
//handles
//'The Great (TV Series 2020– ) - IMDb', '"Homecoming" People (TV Episode 2020) - IMDb', "Saw (2013) - IMDb", 'Homecoming - Season 2 - IMDb', etc.
//All within "Title" if at the start, or up to the last "(", or up to - IMDb suffix, or all of it. Handle possible () and "" inside the title itself too.
return regexGroup(tabTitle, /^(?:"(.*)"|(.*) \(|(.*) - |(.*))/, true);
}
function getImdbId() {
//get just the number after 'tt' from page URL
const url = document.location;
//OR: If viewing an episode, and if episode search fails for most episodes, can hide button
//But, search by episode works too (for some). Viewing episode changes to episode-only IMDB ID, no way to identify TV show ID.
//if (/[?&]ref_=tt_ep/.test(url)) return '';
return regexGroup(url, /\/title\/(tt\d{3,})\//); //OR: 7+ digits
}
//Utility functions:
function escapeForUrl(str) {
return str && encodeURIComponent(str).replace('%20','+') || '';
}
function regexGroup(text, regex, groupNumNameOrNeg1ForFirstMatch, undefInsteadOfEmptyStringForNoMatch) {
let group;
if (text && regex) {
//MAYBE: try, catch & log?
//get match groups for regex
const matches = regex.exec(text);
//regex match debugging:
if (debugRegex) console.error(scriptLogPrefix + ` Regex debug "${regex} match groups:`, matches);
if (matches) {
let groupNum = groupNumNameOrNeg1ForFirstMatch;
if (groupNum === true) {
//return first non-empty group
for (let i = 1; i < matches.length; i++) { //start with first match group (not 0, which is entire matching string)
group = matches[i];
if (group !== undefined) { //or exclude empty string too optionally, eg with param: skipEmptyTextWhenFindingFirst
return group;
}
}
} else if(typeof(groupNum) === 'string' && groupNum !== '') { //find named group
//return a named group:
group = matches.group ? matches.groups[groupNum] : undefined;
} else {
//return numbered group, or default to first captured group
if (typeof(groupNum) !== 'number' || groupNum < 0) {
groupNum = 1; //OR: should we default to 0 for entire matching string? OR: just 0 if negative?
}
group = matches[groupNum];
}
}
}
return !undefInsteadOfEmptyStringForNoMatch && group === undefined ? '' : group;
}
async function loadNumSetting(name, defaultValOrNeg1, skipSavingDefaultIfNotFound) {
//OR: Can leave off requiredType param, and auto convert number, etc. to string?
return loadSetting(name, (defaultValOrNeg1 === undefined ? -1 : defaultValOrNeg1), "number", skipSavingDefaultIfNotFound);
}
async function loadStringSetting(name, defaultValOrEmpty, skipSavingDefaultIfNotFound) {
//OR: Can leave off requiredType param, and auto convert number, etc. to string?
return loadSetting(name, (defaultValOrEmpty === undefined ? '' : defaultValOrEmpty), "string", skipSavingDefaultIfNotFound);
}
async function loadBoolSetting(name, defaultVal, skipSavingDefaultIfNotFound) {
return loadSetting(name, defaultVal, "boolean", skipSavingDefaultIfNotFound);
}
async function loadSetting(name, defaultVal, requiredType, skipSavingDefaultIfNotFound) {
let value = defaultVal;
if (useSettings && name) {
try {
//load the setting from user script storage which user can see and edit
value = await GM_getValue(name);
//NOTE: always converts to/from JSON, so strings appear with quotes around them in Values editor. Editing to empty results in Value entry being deleted.
//saving undefined is converted to null.
//check if not the required type, if known
const notRequiredType = (requiredType && typeof(value) !== requiredType);
//save default, if none was found already (or was invalid or incorrect type), so user can see it to edit it, and know what is being used
if ((value === undefined || notRequiredType)) {
//use default instead
value = defaultVal;
if (!skipSavingDefaultIfNotFound) {
//save it
const saveWaiter = saveSetting(name, value);
//optionally wait for saving to finish, if doing sequential testing
if (awaitSavingDefaults) await saveWaiter;
}
}
} catch (er) {
console.error(scriptLogPrefix + `Failed to load user script setting "${name}" (editable under User Script > "Values" tab) due to error:`, er);
}
}
return value;
}
async function saveSetting(name, value) {
//debug:
console.warn(scriptLogPrefix + `Saving user setting "${name}" with value "${value}"`);
if (!useSettings || !name) return;
try {
//save the setting, so can users can find and edit under User Script > Values
//undefined is converted to null. auto converted to/from JSON, so string has quotes around it. empty (ie. invalid) JSON is auto deleted after user edit
await GM_setValue(name, value);
} catch (er) {
console.error(scriptLogPrefix + `Failed to save user script setting "${name}" with value "${value}" due to error: `, er);
}
}
})();