Power IMDB:Trakt.tv & YouTube Trailers (Powered by PowerSheet.ai 🆓 No-Code Apps, Collab Platform)

"YouTube Trailer" & "View on Trakt.tv" on IMDB navbar for TV & movie rating, watchlist & progress. — Powered by👉🆓 PowerSheet.ai 📊-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 PWAs, Embedded Anywhere — 🆓Sign up@👉 https://PowerSheet.ai

当前为 2020-07-31 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Power IMDB:Trakt.tv & YouTube Trailers (Powered by PowerSheet.ai 🆓 No-Code Apps, Collab Platform)
  3. // @author PowerSheet.ai – 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 PWAs, Embedded Anywhere | Sign up now free @ https://PowerSheet.ai !
  4. // @copyright © 2020 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 PWAs, Embedded Anywhere | Sign up now free @ https://PowerSheet.ai !
  5. // @description "YouTube Trailer" & "View on Trakt.tv" on IMDB navbar for TV & movie rating, watchlist & progress. — Powered by👉🆓 PowerSheet.ai 📊-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 PWAs, Embedded Anywhere — 🆓Sign up@👉 https://PowerSheet.ai
  6. // @version 2.0.3
  7. // @include /^https?://(\w+\.)imdb\.com/title//
  8. // @namespace https://PowerSheet.ai
  9. // @homepageURL https://PowerSheet.ai/Free-NoCode-App-BI-Excel-Blockchain
  10. // @supportURL https://PowerSheet.ai/Free-NoCode-App-BI-Excel-Blockchain
  11. // @contributionURL https://PowerSheet.ai/Free-NoCode-App-BI-Excel-Blockchain
  12. // @updateUrl https://greasyfork.org/scripts/402912-power-imdb-trakt-tv-links-powered-by-powersheet-ai-no-code-app-bi-bot-webext-blockchain-platform
  13. // @license CC-BY-NC-SA-4.0
  14. // Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (https://creativecommons.org/licenses/by-nc-sa/4.0/)
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @inject-into content
  18. // @run-at document-end
  19. // @priority 1
  20. // @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
  21. // @screenshot https://greasyfork.org/system/screenshots/screenshots/000/021/114/original/Power-IMDB-Screenshot.jpg?1590285123
  22. // ==/UserScript==
  23. // User Script metadata docs: https://greasyfork.org/en/help/meta-keys (Name limited to 100 char, Description to 500)
  24.  
  25. // 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.
  26.  
  27. // ● 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
  28. // ● 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.
  29. // ● 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.
  30. // ● Instantly auto publish everywhere as mobile, Web 4.0, Excel, decentralized PWA, desktop, offline and embedded apps.
  31. // ● Collaboratively create and embed in Excel, Microsoft Teams, PowerPoint, SharePoint, web browsers, websites, existing apps and anywhere.
  32. // ● Simultaneously sell and share everywhere with our universal marketplace for smart apps, templates, connectors and content.
  33. // ● Free for unlimited users. No coding, install, server or IT setup required.
  34. // ● Sign up 🆓 @ 👉 https://PowerSheet.ai.
  35.  
  36. (async function () {
  37.  
  38. //constants:
  39.  
  40. const useSettings = true;
  41. const awaitSavingDefaults = false;
  42. const debugRegex = false;
  43. const scriptLogPrefix = '[Power IMDB (User Script)] ';
  44.  
  45. //MAYBE: Add setting to include year in YouTube search (but outside of quotes)?
  46.  
  47. const traktSearchLink = 'https://trakt.tv/search/imdb?q=';
  48. const youTubeTrailerSearchLinkBase = 'https://www.youtube.com/results?search_query=';
  49. const youTubeTrailerSearchLinkSuffix = '+trailer';
  50.  
  51. const buttonPadding = '.3rem'; //reduced padding vs. original 1rem for Watchlist etc buttons
  52. const labelIconPadding = '4px'; //reduced padding vs. original 1rem for Watchlist etc buttons
  53.  
  54. let buttonOrderPos = 8; //ensure it's the last button. auto-incremented
  55. let hideLabels = false;
  56.  
  57. try {
  58.  
  59. //load user settings and state:
  60.  
  61. hideLabels = await loadBoolSetting('hide-labels', false);
  62.  
  63. //image constants:
  64.  
  65. 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">
  66. <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>
  67. </svg>`;
  68.  
  69. //From: https://developers.google.com/site-assets/logo-youtube.svg
  70. 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">
  71. <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"/>
  72. <polygon fill="#fff" points="63.9,79.2 103.2,55 63.9,30.8 "/>
  73. </svg>`;
  74.  
  75.  
  76. //generate HTML for buttons
  77. const traktButton = buttonHtml('Trakt', //label
  78. getTraktTitleUrl(), //url
  79. 'Open Trakt.tv page for Title', //tooltip
  80. 'open-trakt',
  81. traktIconSvg
  82. );
  83. const trailerButton = buttonHtml('Trailer', //label
  84. getYouTubeTrailerSearchLink(), //url
  85. 'Open YouTube trailer video search in new tab', //tooltip
  86. 'open-youtube-trailer',
  87. youTubeIconSvg
  88. );
  89. const linkHtml = traktButton + trailerButton;
  90.  
  91. //add button HTML to navbar
  92. addButtonsToNavBar(linkHtml);
  93.  
  94. } catch(e) {
  95. console.error(scriptLogPrefix + `Error creating or adding buttons or loading settings: `, e);
  96. }
  97.  
  98. //Functions:
  99.  
  100. //returns html defining button for the navbar
  101. function buttonHtml(label, linkUrl, tooltip, idBase, iconSvg) {
  102. //return empty if no URL defined, to skip this button
  103. if (!linkUrl) return '';
  104.  
  105. return `
  106. <div id+"${idBase}-div" style="
  107. order: ${buttonOrderPos};
  108. ">
  109. <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="
  110. padding: 0 ${buttonPadding};
  111. ">
  112. ${iconSvg}
  113. ${hideLabels ? '' : `
  114. <div class="${idBase}-text ipc-button__text" style="
  115. padding-left: ${labelIconPadding};
  116. ">${label}</div>`
  117. }
  118. </a>
  119. </div>
  120. `;
  121. }
  122.  
  123. function addButtonsToNavBar(buttonsHtml) {
  124. if (!buttonsHtml) return;
  125.  
  126. //insert as last child of navbar
  127. let topNav = document.getElementsByClassName('ipc-page-content-container')[0];
  128. if (!topNav) {
  129. 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.');
  130. topNav = document.body || document.documentElement;
  131. //Fallback to adding to top or bottom of page?
  132. }
  133. topNav.insertAdjacentHTML('beforeEnd', buttonsHtml);
  134. }
  135.  
  136. function getTraktTitleUrl() {
  137. const imdbID = getImdbId();
  138. return imdbID && (traktSearchLink + imdbID) || '';
  139. }
  140.  
  141. function getYouTubeTrailerSearchLink() {
  142. let title = getTitle();
  143.  
  144. if (!title) return '';
  145. title = title.replace('"','');
  146. if (!title) return '';
  147. title = '"' + title + '"';
  148. const titleEncoded = escapeForUrl(title);
  149.  
  150. //MAYBE: Setting to enable adding year (if parsed) outside of quotes?
  151.  
  152. return youTubeTrailerSearchLinkBase + titleEncoded + youTubeTrailerSearchLinkSuffix;
  153. }
  154.  
  155. //parse title of TV series or movie (even from episode, etc. page) from tab title
  156. function getTitle() {
  157. const titleMeta = document.querySelector("meta[property='og:title']");
  158. const tabTitle = titleMeta && titleMeta.getAttribute('content');
  159. //handles
  160. //'The Great (TV Series 2020– ) - IMDb', '"Homecoming" People (TV Episode 2020) - IMDb', "Saw (2013) - IMDb", 'Homecoming - Season 2 - IMDb', etc.
  161. //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.
  162. return regexGroup(tabTitle, /^(?:"(.*)"|(.*) \(|(.*) - |(.*))/, true);
  163. }
  164.  
  165. function getImdbId() {
  166. //get just the number after 'tt' from page URL
  167. const url = document.location;
  168.  
  169. //OR: If viewing an episode, and if episode search fails for most episodes, can hide button
  170. //But, search by episode works too (for some). Viewing episode changes to episode-only IMDB ID, no way to identify TV show ID.
  171. //if (/[?&]ref_=tt_ep/.test(url)) return '';
  172.  
  173. return regexGroup(url, /\/title\/(tt\d{3,})\//); //OR: 7+ digits
  174. }
  175.  
  176.  
  177. //Utility functions:
  178.  
  179. function escapeForUrl(str) {
  180. return str && encodeURIComponent(str).replace('%20','+') || '';
  181. }
  182.  
  183. function regexGroup(text, regex, groupNumNameOrNeg1ForFirstMatch, undefInsteadOfEmptyStringForNoMatch) {
  184. let group;
  185. if (text && regex) {
  186. //MAYBE: try, catch & log?
  187.  
  188. //get match groups for regex
  189. const matches = regex.exec(text);
  190.  
  191. //regex match debugging:
  192. if (debugRegex) console.error(scriptLogPrefix + ` Regex debug "${regex} match groups:`, matches);
  193.  
  194. if (matches) {
  195. let groupNum = groupNumNameOrNeg1ForFirstMatch;
  196. if (groupNum === true) {
  197. //return first non-empty group
  198. for (let i = 1; i < matches.length; i++) { //start with first match group (not 0, which is entire matching string)
  199. group = matches[i];
  200. if (group !== undefined) { //or exclude empty string too optionally, eg with param: skipEmptyTextWhenFindingFirst
  201. return group;
  202. }
  203. }
  204. } else if(typeof(groupNum) === 'string' && groupNum !== '') { //find named group
  205. //return a named group:
  206. group = matches.group ? matches.groups[groupNum] : undefined;
  207. } else {
  208. //return numbered group, or default to first captured group
  209. if (typeof(groupNum) !== 'number' || groupNum < 0) {
  210. groupNum = 1; //OR: should we default to 0 for entire matching string? OR: just 0 if negative?
  211. }
  212. group = matches[groupNum];
  213. }
  214. }
  215. }
  216. return !undefInsteadOfEmptyStringForNoMatch && group === undefined ? '' : group;
  217. }
  218.  
  219. async function loadNumSetting(name, defaultValOrNeg1, skipSavingDefaultIfNotFound) {
  220. //OR: Can leave off requiredType param, and auto convert number, etc. to string?
  221. return loadSetting(name, (defaultValOrNeg1 === undefined ? -1 : defaultValOrNeg1), "number", skipSavingDefaultIfNotFound);
  222. }
  223. async function loadStringSetting(name, defaultValOrEmpty, skipSavingDefaultIfNotFound) {
  224. //OR: Can leave off requiredType param, and auto convert number, etc. to string?
  225. return loadSetting(name, (defaultValOrEmpty === undefined ? '' : defaultValOrEmpty), "string", skipSavingDefaultIfNotFound);
  226. }
  227. async function loadBoolSetting(name, defaultVal, skipSavingDefaultIfNotFound) {
  228. return loadSetting(name, defaultVal, "boolean", skipSavingDefaultIfNotFound);
  229. }
  230. async function loadSetting(name, defaultVal, requiredType, skipSavingDefaultIfNotFound) {
  231.  
  232. let value = defaultVal;
  233.  
  234. if (useSettings && name) {
  235. try {
  236. //load the setting from user script storage which user can see and edit
  237. value = await GM_getValue(name);
  238. //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.
  239. //saving undefined is converted to null.
  240.  
  241. //check if not the required type, if known
  242. const notRequiredType = (requiredType && typeof(value) !== requiredType);
  243.  
  244. //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
  245. if ((value === undefined || notRequiredType)) {
  246. //use default instead
  247. value = defaultVal;
  248. if (!skipSavingDefaultIfNotFound) {
  249. //save it
  250. const saveWaiter = saveSetting(name, value);
  251. //optionally wait for saving to finish, if doing sequential testing
  252. if (awaitSavingDefaults) await saveWaiter;
  253. }
  254. }
  255.  
  256. } catch (er) {
  257. console.error(scriptLogPrefix + `Failed to load user script setting "${name}" (editable under User Script > "Values" tab) due to error:`, er);
  258. }
  259. }
  260. return value;
  261. }
  262.  
  263. async function saveSetting(name, value) {
  264. //debug:
  265. console.warn(scriptLogPrefix + `Saving user setting "${name}" with value "${value}"`);
  266.  
  267. if (!useSettings || !name) return;
  268.  
  269. try {
  270. //save the setting, so can users can find and edit under User Script > Values
  271. //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
  272. await GM_setValue(name, value);
  273. } catch (er) {
  274. console.error(scriptLogPrefix + `Failed to save user script setting "${name}" with value "${value}" due to error: `, er);
  275. }
  276. }
  277.  
  278.  
  279. })();