// ==UserScript== // @name Spoiler-free Crunchyroll // @description Hide name, image, and description of episodes // @author TimeBomb // @namespace https://greasyfork.org/users/160017 // @version 0.8 // @copyright 2022 // @run-at document-start // @match https://*.crunchyroll.com/* // @downloadURL none // ==/UserScript== // USER CONFIGS BEGIN const USER_CONFIG = { EPISODE_IMAGES: true, // true: Blur episode images on Continue Watching and Your Watchlist and Series pages and Next/Previous episode EPISODE_NAMES: true, // true: Blur episode names on Continue Watching and Your Watchlist and Series pages and Next/Previous episode. PLAYER_EPISODE_NAME: true, // true: Blur episode name that you're currently watching TITLE_EPISODE_NAME: true, // true: Censors episode name from the title of the page (visible in your browser tab). Series name and episode number still visible. TOOLTIPS: true, // true: Censor episode name when hovering over certain parts of the website that show episode name in a tooltip. WARNING: This may very slightly impact performance while the site loads. Might be noticeable on old machines. CENSOR_URLS_WITH_EPISODE_NAME: false, // true: Censors URL (replaces it) when viewing episode. Off by default, change false to true to enable it. WARNING: This will modify your browser history. (This works on any page with "/watch/" in the URL) }; // USER CONFIGS END, DO NOT EDIT ANYTHING BELOW const DEBUG = true; try { console.log('Spoiler-free Crunchyroll script loaded') // We very briefly hide the tag here, to ensure the user doesn't see unfiltered content // The performance impact of applying our custom CSS is so minimal that users shouldn't notice this // Once we finish applying our CSS below, we show the page and apply some final filters to truncate episode names that contain the episode number or link document.documentElement.style.display = 'none'; // Developer Note: // We are extra performant because most of our filters are just CSS we apply to the prior to loading. // We avoid jQuery and try to avoid function calls for performance's sake. // Previous, less optimized versions of this script noticably slowed down the page; our performance is great as of 0.3 though. // Super fragile custom CSS incoming, good luck if Crunchyroll changes their DOM. let cssE = ''; if (USER_CONFIG.EPISODE_IMAGES) { cssE = cssE + '.card figure { filter: blur(20px) }'; cssE = cssE + '[data-t="watch-list-card"] figure { filter: blur(20px) }'; cssE = cssE + '[data-t="playable-card-mini"] figure { filter: blur(20px); }' } if (USER_CONFIG.EPISODE_NAMES) { cssE = cssE + '.card h4 a { filter: blur(20px) }'; cssE = cssE + '[data-t="watch-list-card"] h5 { filter: blur(6px) }'; cssE = cssE + '[data-t="playable-card-mini"] h4 a { filter: blur(10px); }' } if (USER_CONFIG.PLAYER_EPISODE_NAME) { cssE = cssE + '.current-media-wrapper h1 { filter: blur(12px) }'; } try { var $newStyleE = document.createElement('style'); var cssNodeE = document.createTextNode(cssE); $newStyleE.appendChild(cssNodeE); document.head.appendChild($newStyleE); } catch (e) { if (DEBUG) { console.error('[Spoiler-free Crunchyroll Script DEBUG] CSS Error:', e); } } document.documentElement.style.display = 'inherit'; if (USER_CONFIG.CENSOR_URLS_WITH_EPISODE_NAME) { if (location.href.includes('/watch/')) { window.history.replaceState(null, '', 'censored'); } } function censorDocTitle() { // Set episode+series name based off specific elements const episodeRegex = /Watch on Crunchyroll$/; const censoredTitle = '[Episode Name Censored] - Watch on Crunchyroll'; const $episodeName = document.querySelector('.erc-current-media-info h1.title'); const $seriesName = document.querySelector('.show-title-link h4, .hero-heading-line h1'); // show-title-link is series name on episode player page, .hero-heading-line is series name on series episode list page let episodeName = false; let episodeNumber = false; let seriesName = $seriesName?.textContent ?? false; // Grab episode name from the player page. Expecting format like: "E1 - Episode name here" if ($episodeName?.textContent) { episodeName = $episodeName.textContent.split(' - '); if (episodeName.length > 0) { episodeNumber = episodeName[0]; episodeName = episodeName[1]; } else { if (DEBUG) { console.warn('[Spoiler-free Crunchyroll Script DEBUG] Unable to censor episode name in document title, received unexpected episode name format:', $episodeName.textContent) } } } // Update document.title based off the above episode and series name vars let newDocTitle; if (document.title !== censoredTitle && episodeRegex.test(document.title)) { if (DEBUG) { console.log('[Spoiler-free Crunchyroll Script DEBUG] Censoring document.title, original is:', document.title, 'episode name is:', episodeName); } if (!!seriesName) { if (episodeNumber !== false) { newDocTitle = `${seriesName} ${episodeNumber} - Watch on Crunchyroll`; } else { newDocTitle = `${seriesName} - Watch on Crunchyroll`; } } else { if (DEBUG) { console.warn('[Spoiler-free Crunchyroll Script DEBUG] Unable to include series name in title of censored episode, series name not found on page'); } // We still censor the document title even if we don't know the episode name - we err on the side of preferring to censor. newDocTitle = '[Censored Episode Name] - Watch on Crunchyroll'; } } if (newDocTitle && newDocTitle !== document.title) { document.title = newDocTitle; } } if (USER_CONFIG.TITLE_EPISODE_NAME) { // Observe when document title changes, so we can instantly censor it const target = document.querySelector('head > title'); const observer = new MutationObserver(censorDocTitle); observer.observe(target, { subtree: true, characterData: true, childList: true }); censorDocTitle(); } function censorTooltips() { const $elements = document.querySelectorAll('.card div a[title], [data-t="playable-card-mini"] a[title], a.erc-up-next-section[title], [data-t="watch-list-card"] a[title]'); $elements.forEach($elementWithTitle => { const title = $elementWithTitle.getAttribute('title'); let seasonEpisodeNum = title.split(' - '); seasonEpisodeNum = seasonEpisodeNum.length > 0 ? seasonEpisodeNum[0] : false; if (seasonEpisodeNum && seasonEpisodeNum !== title) { $elementWithTitle.setAttribute('title', seasonEpisodeNum); } }); } // We need to do some things when the HTML on the page finishes loading, e.g. grab the series name to put it in the document title document.addEventListener('DOMContentLoaded', function () { if (USER_CONFIG.TITLE_EPISODE_NAME) { censorDocTitle(); } if (USER_CONFIG.TOOLTIPS) { const target = document.querySelector('.app-body-wrapper'); const observer = new MutationObserver(censorTooltips); observer.observe(target, { subtree: true, characterData: true, childList: true }); censorTooltips(); } }); } catch (e) { console.error('Error with Spoiler-free Crunchyroll script. If this causes noticeable issues, please leave feedback on the greasyfork page and include this error:', e); throw e; }