Greasy Fork is available in English.

Spoiler-free Crunchyroll

Hide name, image, and description of episodes

// ==UserScript==
// @name          Spoiler-free Crunchyroll
// @description   Hide name, image, and description of episodes
// @author        TimeBomb
// @namespace     https://greasyfork.org/users/160017
// @version       0.9
// @copyright     2022
// @run-at        document-start
// @match         https://www.crunchyroll.com/*
// ==/UserScript==

// USER CONFIGS BEGIN
const USER_CONFIG = {
    // true: Blur episode images on Continue Watching and Your Watchlist and Series pages and Next/Previous episode
    EPISODE_IMAGES: true,

    // true: Blur episode names on Continue Watching and Your Watchlist and Series pages and Next/Previous episode.
    EPISODE_NAMES: true,

    // true: Blur episode name that you're currently watching
    PLAYER_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.
    TITLE_EPISODE_NAME: 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.
    TOOLTIPS: true,

    // 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)
    CENSOR_URLS_WITH_EPISODE_NAME: false,
};
// USER CONFIGS END, DO NOT EDIT ANYTHING BELOW

const DEBUG = false;

try {
    console.log('Spoiler-free Crunchyroll script loaded')

    // We very briefly hide the <html> 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 <head> 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';

    function censorUrl() {
        if (location.href.includes('/watch/')) {
            window.setTimeout(() => window.history.replaceState(null, '', 'censored'), 10);
        }
    }

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

        if (USER_CONFIG.CENSOR_URLS_WITH_EPISODE_NAME) {
            censorUrl();
            function tryCensorUrlOnUrlChange() {
                let lastUrl = window.location.href;
                window.setInterval(() => {
                    if (lastUrl !== window.location.href) {
                        lastUrl = window.location.href;
                        censorUrl();
                    }
                }, 10);
            }
            tryCensorUrlOnUrlChange();
        }
    });
} catch (e) {
    console.error('[Spoiler-Free Crunchyroll Script] There was an error loading the script. If this causes noticeable issues, please leave feedback on the greasyfork page and include this error:', e);
    throw e;
}