Bye Spoilers - Crunchyroll

Censor episode's titles, thumbnails, descriptions and tooltips on Crunchyroll. Skips in-video titles (in dev progress). In other words, you'll avoid spoilers.

// ==UserScript==
// @name           Bye Spoilers - Crunchyroll
// @name:es        Bye Spoilers - Crunchyroll
// @namespace      https://github.com/zAlfok/ByeSpoilers-Crunchyroll
// @match          https://www.crunchyroll.com/*
// @match          https://static.crunchyroll.com/vilos-v2/web/vilos/player.html
// @grant          none
// @version        1.2.3
// @license        GPL-3.0
// @author         Alfok
// @description    Censor episode's titles, thumbnails, descriptions and tooltips on Crunchyroll. Skips in-video titles (in dev progress). In other words, you'll avoid spoilers.
// @description:es Censura los títulos, miniaturas, descripciones, URLs y 'tooltips' de los episodios en Crunchyroll. Salta el título del episodio en el video (en progreso de desarrollo). En otras palabras, evitarás spoilers.
// @icon           https://raw.githubusercontent.com/zAlfok/ByeSpoilers-Crunchyroll/master/assets-images/logov2.png
// @run-at         document-start
// @resource       TITLE_INTERVALS_JSON  https://github.com/zAlfok/ByeSpoilers-Crunchyroll/raw/master/scripts/crunchyroll_titles_intervals_compactSimplified.json
// @grant          GM_getResourceText
// @homepageURL    https://github.com/zAlfok/ByeSpoilers-Crunchyroll
// @supportURL     https://github.com/zAlfok/ByeSpoilers-Crunchyroll/issues
// ==/UserScript==

// ------------------------------------------------------------------------------------------------------------------
// To customize the script, change the USER_CONFIG object below.
// USER CONFIGS BEGIN
const debugEnable = false; // In order to see what's happening in the script, set this to true. It will log messages to the console.
const USER_CONFIG = {
    // true: Fetch the JSON file instead of using the resource (default is false), 
    // this is works together with SKIP_EPISODE_TITLES
    // Tampermonkey has trouble with GM_getResourceText, so it's better to use fetch 
    // (just try with false first and if it doesn't work, set it to true)
    // Violentmonkey supports GM_getResourceText, so it's better to use it, to avoid 
    // downloading the file every time, however, in this initial phase could be better
    // considering that the file will be updated frequently
    FETCH_INSTEAD_OF_RESOURCE: false, 
    // true: Skip in-video episode titles (in development, default is false)
    SKIP_EPISODE_TITLES: false, 
    // true: Blur episode thumbnails on the following pages:
    // /home: Continue Watching Grid, Watchlist Grid (Hover), 
    // /watchlist: Grid of Episodes (Hover)
    // /history: Grid of Episodes
    // /series: Last Episode, Grid of Episodes
    // /watch: Next/Previous Episode, See More Episodes (Side and PopUp)
    BLUR_EPISODE_THUMBNAILS: true,

    // true: Blur episodes title on the following pages:
    // /home: Continue Watching Grid, Watchlist Grid (Hover), 
    // /watchlist: Grid of Episodes (Hover)
    // /history: Grid of Episodes
    // /series: Last Episode, Grid of Episodes
    // /watch: Next/Previous Episode, See More Episodes (Side and PopUp)
    BLUR_EPISODE_TITLES: false,

    // true: Modify episodes title to "(S#) E# - [Title Censored]" on the following pages:
    // /home: Continue Watching Grid, Continue Watching Grid (Hover), Watchlist Grid (if modifyActive is true, default is false since it's not necessary)
    // /watchlist: Grid of Episodes (if modifyActive is true, default is false since it's not necessary)
    // /history: Grid of Episodes
    // /series: Grid of Episodes, Grid of Episodes (Hover)
    // /watch: Main Title, Next/Previous Episode, See More Episodes (Side and PopUp)
    MODIFY_INSITE_EPISODE_TITLES: true,

    // true: Modify episodes title to "Anime E# - Watch on Crunchyroll" from the tab of your browser. 
    MODIFY_DOCTITLE_EPISODE_TITLE: true,

    // true: Modify episodes title when hovering over certain elements of the page to "(S#) E# - [Title Censored]":
    // /home: Continue Watching Grid
    // /watchlist: Grid of Episodes (Has to be fixed)
    // /history: Grid of Episodes
    // /series: Last Episode, Grid of Episodes 
    // /watch: Next/Previous Episode, See More Episodes (Side and PopUp)
    MODIFY_TOOLTIPS: true,

    // true: Modify URL (replaces it) if episode URL detected. WARNING: This will modify your browser history.
    MODIFY_URL_EPISODE_TITLE: true,

    // true: Blur episode description on the following pages:
    // /home: Continue Watching Grid (Hover)
    // /series: Grid of Episodes (Hover)
    // /watch: Episode Description
    BLUR_EPISODE_DESCRIPTION: true,

    // true: Removes elements related to premium trial:
    // Menu bar "TRY FREE PREMIUM" Button, Banner under player (/watch)
    HIDE_PREMIUM_TRIAL: false
};
// USER CONFIGS END, DO NOT EDIT ANYTHING BELOW IF NOT KNOWING WHAT YOU'RE DOING
// -----------------------------------------------------------------------------------------------------------------

// Global variables to know if relevant elements have been censored
let docTitleCensored = false;
let urlCensored = false;
let titleCensored = false;
// CSS string to apply to the page
let cssE = '';
let titleIntervals = {};
// List of CSS selectors to apply most of the changes (except for the tooltips)
// blurActive and modifyActive control which elements should be blurred and/or modified, advanced control if want to allow certain elements )
const cssSelectorList = {
    "THUMBNAILS": {
        "EP-IMG_HOME-CONT-WATCH_ANIME-LIST_EP-SEE-MORE-POP": {
            selector: '.card figure',
            blurAmount: 20,
            blurActive: true,
            modifyActive: false
        },
        "EP-IMG_HOME-WATCHLIST-HOVER_LIST-WATCHLIST-HOVER": {
            selector: '[data-t="watch-list-card"] .watchlist-card-image__playable-thumbnail--4RQJC figure',
            blurAmount: 20,
            blurActive: true,
            modifyActive: false
        },
        "EP-IMG_EP-NEXT_EP-PREV_EP-SEE-MORE-SIDE": {
            selector: '[data-t="playable-card-mini"] figure',
            blurAmount: 20,
            blurActive: true,
            modifyActive: false
        },
        "EP-IMG_ANIME-INIT": {
            selector: '.up-next-section figure',
            blurAmount: 20,
            blurActive: true,
            modifyActive: false
        },
        "EP-IMG_LIST-HISTORY": {
            selector: '.erc-my-lists-item a .content-image-figure-wrapper__figure-sizer--SH2-x figure',
            blurAmount: 20,
            blurActive: true,
            modifyActive: false
        }
    },
    "TITLES": {
        "EP-TIT_HOME-CONT-WATCH_ANIME-LIST_EP-SEE-MORE-POP": {
            selector: '.card h4 a',
            blurAmount: 20,
            blurActive: true,
            modifyActive: true
        },
        "EP-TIT_HOME-WATCHLIST_LIST-WATCHLIST": {
            selector: '[data-t="watch-list-card"] h5',
            blurAmount: 6,
            blurActive: true,
            modifyActive: false
        },
        "EP-TIT_EP-NEXT_EP-PREV_EP-SEE-MORE-SIDE": {
            selector: '[data-t="playable-card-mini"] h4 a',
            blurAmount: 10,
            blurActive: true,
            modifyActive: true
        },
        "EP-TIT_LIST-HISTORY": {
            selector: '.erc-my-lists-item h4 a',
            blurAmount: 10,
            blurActive: true,
            modifyActive: true
        },
        "EP-TIT_PLAYER": {
            selector: '.current-media-wrapper h1',
            blurAmount: 20,
            blurActive: true,
            modifyActive: true
        },
        "EP-TIT_HOME-CONT-WATCH-HOVER_ANIME-LIST-HOVER": {
            selector: '.card [data-t="episode-title"]',
            blurAmount: 10,
            blurActive: true,
            modifyActive: true
        }
    },
    "DESCRIPTIONS": {
        "EP-DESCR_PLAYER_EPISODE": {
            selector: '.expandable-section__wrapper--G-ttI p',
            blurAmount: 20,
            blurActive: true,
            modifyActive: false
        },
        "EP-DESCR_PLAYER_SERIES": { //needed since had to be more specific (afterwards) to avoid bluring on series description
            selector: '.erc-show-description .expandable-section__wrapper--G-ttI p',
            blurAmount: 0,
            blurActive: true,
            modifyActive: false
        },
        "EP-DESCR_HOME-WATCHLIST-HOVER_ANIME-LIST-HOVER": {
            selector: '.card [data-t="description"]',
            blurAmount: 10,
            blurActive: true,
            modifyActive: false
        }
    }
};
const langList_episodeRegexList = {
    "ar": /شاهد على كرانشي رول$/,
    "de": /Schau auf Crunchyroll$/,
    "en": /Watch on Crunchyroll$/,
    "es": /Ver en Crunchyroll en español$/,
    "es-es": /Ver en Crunchyroll en castellano$/,
    "fr": /Regardez sur Crunchyroll$/,
    "it": /Guardalo su Crunchyroll$/,
    "pt-br": /Assista na Crunchyroll$/,
    "pt-pt": /Assiste na Crunchyroll$/,
    "ru": /смотреть на Crunchyroll$/,
    "hi": /क्रंचीरोल पर देखें$/
}
// CSS just for bluring/hiding elements 
function concatStyleCSS() {
    debugEnable && console.log(USER_CONFIG.BLUR_EPISODE_THUMBNAILS ? "BLUR_EPISODE_THUMBNAILS: ON" : "BLUR_EPISODE_THUMBNAILS: OFF");      
    if (USER_CONFIG.BLUR_EPISODE_THUMBNAILS) {
        for (let key in cssSelectorList["THUMBNAILS"]) {
            let item = cssSelectorList["THUMBNAILS"][key];
            if (item.blurActive) {
                cssE = cssE + `${item.selector} { filter: blur(${item.blurAmount}px); }`;
            }
        }
    }
    debugEnable && console.log(USER_CONFIG.BLUR_EPISODE_TITLES ? "BLUR_EPISODE_TITLES: ON" : "BLUR_EPISODE_TITLES: OFF");
    if (USER_CONFIG.BLUR_EPISODE_TITLES) {
        for (let key in cssSelectorList["TITLES"]) {
            let item = cssSelectorList["TITLES"][key];
            if (item.blurActive) {
                cssE = cssE + `${item.selector} { filter: blur(${item.blurAmount}px); }`;
            }
        }
    }
    debugEnable && console.log(USER_CONFIG.BLUR_EPISODE_DESCRIPTION ? "BLUR_EPISODE_DESCRIPTION: ON" : "BLUR_EPISODE_DESCRIPTION: OFF");
    if (USER_CONFIG.BLUR_EPISODE_DESCRIPTION) {
        for (let key in cssSelectorList["DESCRIPTIONS"]) {
            let item = cssSelectorList["DESCRIPTIONS"][key];
            if (item.blurActive) {
                cssE = cssE + `${item.selector} { filter: blur(${item.blurAmount}px); }`;
            }
        }
    }
    debugEnable && console.log(USER_CONFIG.HIDE_PREMIUM_TRIAL ? "HIDE_PREMIUM_TRIAL: ON, some things will be executed by modifying on mainLogic" : "HIDE_PREMIUM_TRIAL: OFF");
    if (USER_CONFIG.HIDE_PREMIUM_TRIAL) {
        cssE = cssE + '.erc-user-actions > :first-child, .banner-wrapper, .button-wrapper { display: none; }';
        // cssE = cssE + 'vsc-initialized { height: 0%};'; // Not 0% in all cases, it's done on mainLogic, kept here for reference
    }
}
// Gets the serie's name and the episode's number and title from the episode page
function getEpisodeTitleFromEpisodeSite() {
    debugEnable && console.log("[getEpisodeTitleFromEpisodeSite]: Getting episode title from episode site");
    const $episodeTitle = document.querySelector('.erc-current-media-info h1.title, .card h4 a ');
    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 episodeTitle = "";
    let episodeNumber = "";
    let seriesName = $seriesName?.textContent ?? "";
    if ($episodeTitle?.textContent) {
        episodeTitle = $episodeTitle.textContent.split(' - ');
        if (episodeTitle.length > 0) {
            debugEnable && console.log('[getEpisodeTitleFromEpisodeSite]: Episode title with separator: ', $episodeTitle.textContent);
            episodeNumber = episodeTitle[0];
            episodeTitle = episodeTitle[1];
        } else {
            debugEnable && console.log('[getEpisodeTitleFromEpisodeSite]: Episode title without separator: ', $episodeTitle.textContent);
        }
    } else {
        debugEnable && console.warn('[getEpisodeTitleFromEpisodeSite]: Episode title not found');
    }
    return [episodeNumber, episodeTitle, seriesName];
}
// Censor the URL only on episode pages
function censorUrl() {
    let [episodeNumber, episodeTitle, seriesName] = getEpisodeTitleFromEpisodeSite();
    debugEnable && console.log(`[censorUrl]: New title: censored-${seriesName.replace(/ /g, "_")}-${episodeNumber}`);
    window.history.replaceState(null, '', `censored-${seriesName.replace(/ /g, "_")}-${episodeNumber}`);
    urlCensored = true;
    debugEnable && console.log("[censorUrl]: URL censored");
    
    if (docTitleCensored && titleCensored) {
        document.documentElement.style.filter = 'none';
    }
}
// Censor the document title (browser's taba) only on episode pages
function censorDocTitle() {
    const crunchyLang = document.documentElement.lang;
    const episodeRegex = langList_episodeRegexList[crunchyLang] || langList_episodeRegexList["en"];
    const [episodeNumber, episodeTitle, seriesName] = getEpisodeTitleFromEpisodeSite();

    if (document.title.includes("[Title Censored]")) {
        debugEnable && console.log("[censorDocTitle]: Title already censored");
        return;
    }
    const titleSuffix = episodeRegex.source.replace('\$', "");
    let newTitle = "[Title Censored] - " + titleSuffix;
    if (!!seriesName && !!episodeNumber) {
        newTitle = `${seriesName} Episode ${episodeNumber} [Title Censored] - ${titleSuffix}`;
    } else if (!!seriesName) {
        newTitle = `${seriesName} [Title Censored] - ${titleSuffix}`;
    } else if (!!episodeNumber) {
        newTitle = `Episode ${episodeNumber} [Title Censored] - ${titleSuffix}`;
    }
    debugEnable && console.log("[censorDocTitle]: New title: ", newTitle);
    document.title = newTitle;
    docTitleCensored = true;
    debugEnable && console.log("[censorDocTitle]: Title censored");

    if (titleCensored && (isEpisodePage() && urlCensored)) {
        document.documentElement.style.filter = 'none';
    }
}
// Censor tooltips with episode titles (exlusion made on mainLogic for watchlist page)
function censorTooltips() {
    const tooltipTitles = document.querySelectorAll(
        '.card div a[title], ' + // TOOLTIPS_HOME-CONT-WATCH_ANIME-LIST_EP-SEE-MORE-POP
        '[data-t="playable-card-mini"] a[title], ' + //TOOLTIPS_EP-NEXT_EP-PREV_EP-SEE-MORE-SIDE
        '.erc-my-lists-item a[title], ' + //TOOLTIPS_WATCHLIST_HISTORY
        '.erc-series-hero a[title] '  //TOOLTIPS_SERIES
    );
    if (tooltipTitles.length === 0) {
        debugEnable && console.log("[censorTooltips]: No elements found with title attribute");
        return;
    }
    tooltipTitles.forEach(element => {
        const originalTitle = element.getAttribute('title');

        if (originalTitle.includes('[Title Censored]')) {
            debugEnable && console.log("[censorTooltips]: Title already censored");
            return;
        }
        parts = originalTitle.split(' - ');
        let newTitle = parts.length > 1 ? parts[0]+" - [Title Censored]" : "[Title Censored]";
        debugEnable && console.log("[censorTooltips]: New title: ", newTitle);
        element.setAttribute('title', newTitle);
        debugEnable && console.log("[censorTooltips]: Title censored");
    });
    debugEnable && console.log("[censorTooltips]: Censored all elements with title attribute");
}
// Group of functions to determine the current page
function isHomePage() {
    let currentPath = window.location.pathname;
    // Extract keys from the object and build a regular expression (in case of more languages in the future)
    const validPaths = Object.keys(langList_episodeRegexList).map(key => `/${key}/`);
    const isValid = currentPath === "/" || validPaths.includes(currentPath) || validPaths.includes(`${currentPath}/`);
    debugEnable && console.log("[isHomePage]: Current path is valid: ", isValid, " - Current path is: ", currentPath);
    return isValid;
}
function isSeriesPage() {
    let currentPath = window.location.pathname;
    let isValid = currentPath.includes('/series');
    debugEnable && console.log("[isSeriesPage]: Current path is valid: ", isValid, " - Current path is: ", currentPath);
    return isValid;
}
function isHistoryPage() {
    let currentPath = window.location.pathname;
    let isValid = currentPath.includes('/history');
    debugEnable && console.log("[isHistoryPage]: Current path is valid: ", isValid, " - Current path is: ", currentPath);
    return isValid;
}
function isEpisodePage() {
    let currentPath = window.location.pathname;
    let isValid = currentPath.includes('/watch/');
    debugEnable && console.log("[isEpisodePage]: Current path is valid: ", isValid, " - Current path is: ", currentPath);
    return isValid;
}
function isWatchlistPage() {
    let currentPath = window.location.pathname;
    let isValid = currentPath.includes('/watchlist');
    debugEnable && console.log("[isWatchlistPage]: Current path is valid: ", isValid, " - Current path is: ", currentPath);
    return isValid;
}
function isOtherPage() {
    let currentPath = window.location.pathname;
    // Exctract keys from the object and build a regular expression (in case of more languages in the future)
    let validPaths = Object.keys(langList_episodeRegexList).map(key => `/${key}/`);
    let isValidHome = currentPath === "/" || validPaths.includes(currentPath) || validPaths.includes(`${currentPath}/`);
    let isValidOtherFunctions = currentPath.includes("/series") || currentPath.includes("/history") || currentPath.includes("/watch/") || currentPath.includes("/watchlist");
    let isValid = !isValidHome && !isValidOtherFunctions;
    debugEnable && console.log("[isOtherPage]: Current path is valid: ", isValid, " - Current path is: ", currentPath);
    return isValid;
}
// Generic function to censor titles if have (' - ') separator or not from any of the cssSelectorList["TITLES"] selectors
function censorTitleGeneric(selector) {
    const elementsWithTitle = document.querySelectorAll(selector);
    if (elementsWithTitle.length === 0) {
        debugEnable && console.log("[censorTitleGeneric]: No elements found with selector: ", selector);
        return;
    }
    elementsWithTitle.forEach(element => {
        const content = element.textContent;
        if (content.includes("[Title Censored]")) {
            debugEnable && console.log("[censorTitleGeneric]: Title already censored");
            return; 
        }
        const parts = content.split(" - ");
        let newContent = parts.length > 1 ? parts[0] + " - [Title Censored]" : "[Title Censored]";
        element.textContent = newContent;
    });
    debugEnable && console.log("[censorTitleGeneric]: Censored all elements with selector: ", selector);

    titleCensored = true;
    if (docTitleCensored && (isEpisodePage() && urlCensored)) {
        document.documentElement.style.filter = 'none';
    }
}
// Determines if the user is logged in
function isLogged() {
    if (document.querySelector('.user-menu-account-section')) {
        debugEnable && console.log('[isLogged]: User is logged in');
        return true;
    } else {
        debugEnable && console.log('[isLogged]: User is NOT logged in');
        return false;
    }
}
// Main code block
function mainLogic() {
    debugEnable && console.log("[mainLogic]: START");
    const homeContinueWatching = document.querySelector('.erc-feed-continue-watching-item');
    const historyListSite = document.querySelector('.erc-history-content')
    let notLogged = !isLogged();
    let notHomeContinueWatchingOnHomePage = isHomePage() && !homeContinueWatching;
    let notHistoryListSiteOnHistoryPage = isHistoryPage() && !historyListSite;
    // If not logged (no censorable elements) or not home continue watching on home page (no censorable elements) 
    // or not history list site on history page (no censorable elements), then remove blur effect
    debugEnable && console.log("[mainLogic]: Has to remove blur since nothing detected?\nNot logged: ", notLogged, "\nnot home continue watching on home page: ", notHomeContinueWatchingOnHomePage, "\nnot history list site on history page: ", notHistoryListSiteOnHistoryPage);
    if (notLogged || notHomeContinueWatchingOnHomePage || notHistoryListSiteOnHistoryPage) {
        document.documentElement.style.filter = 'none';
        debugEnable && console.log("[mainLogic]: Has to remove blur since nothing detected? Yes. No censorable elements detected. Removing blur effect.");
    } else {
        debugEnable && console.log("[mainLogic]: Has to remove blur since nothing detected? No. Censorable elements detected. Evaluating if blur effect should be applied (again).");
        // If it's supposed to censor, apply blur effect back untils all censoring is done (lines below)
        // Verification if title should be censored (only on home, history, series and episode pages)
        let isTitleCensorshipNeeded = isHomePage() || isHistoryPage() || isSeriesPage() || isEpisodePage();
        let isTitleCensoredCorrectly = !isTitleCensorshipNeeded || titleCensored;

        // Verification if URL should be censored (only on episode pages)
        let isUrlCensorshipNeeded = isEpisodePage();
        let isUrlCensoredCorrectly = !isUrlCensorshipNeeded || urlCensored;

        // Verification if document title should be censored (only on episode pages)
        let isDocTitleCensorshipNeeded = isEpisodePage();
        let isDocTitleCensoredCorrectly = !isDocTitleCensorshipNeeded || docTitleCensored;

        // Final verification if blur effect should be applied again
        if (!isTitleCensoredCorrectly || !isUrlCensoredCorrectly || !isDocTitleCensoredCorrectly) {
            document.documentElement.style.filter = 'blur: 2px;';
            debugEnable && console.log("[mainLogic]: One or more censoring conditions are not met. Applying blur effect again.");
        } else {
            debugEnable && console.log("[mainLogic]: All censoring conditions are met. Not necessary to apply blur effect again.");
        }
    }
    // Makes sure that blur effect is removed when all censoring is done (if censoring wasn't needed, it's removed before)
    if  (
            (isHomePage() && (USER_CONFIG.MODIFY_INSITE_EPISODE_TITLES ? titleCensored : true)) ||
            (isEpisodePage() && (USER_CONFIG.MODIFY_INSITE_EPISODE_TITLES ? titleCensored : true) && 
                                (USER_CONFIG.MODIFY_DOCTITLE_EPISODE_TITLE ? docTitleCensored : true) &&
                                (USER_CONFIG.MODIFY_URL_EPISODE_TITLE ? urlCensored : true)) ||
            (isSeriesPage() && (USER_CONFIG.MODIFY_INSITE_EPISODE_TITLES ? titleCensored : true)) ||
            (isHistoryPage() && (USER_CONFIG.MODIFY_INSITE_EPISODE_TITLES ? titleCensored : true)) ||
            (isWatchlistPage()) ||
            (isOtherPage())
        ){
        document.documentElement.style.filter = 'blur(0px)';
        debugEnable && console.log("[mainLogic]: All needed censorship done. Removing blur effect");
    }
    // Not working at css <style> level, therefore it's done here on each document change to ensure it's applied
    debugEnable && console.log(USER_CONFIG.HIDE_PREMIUM_TRIAL ? "HIDE_PREMIUM_TRIAL (cont): ON, remaining stuff" : "HIDE_PREMIUM_TRIAL (cont): OFF");
    if (USER_CONFIG.HIDE_PREMIUM_TRIAL) {
        const botonWrapper = document.querySelector('.button-wrapper');
        if (botonWrapper) {
            botonWrapper.style.display = 'none';
            debugEnable && console.log("[mainLogic]: HIDE_PREMIUM_TRIAL: Drop-down menu button removed");
        }
        const iconWrapper = document.querySelector('.erc-user-actions > :first-child');
        if (iconWrapper) {
            iconWrapper.style.display = 'none';
            debugEnable && console.log("[mainLogic]: HIDE_PREMIUM_TRIAL: Top navigation bar icon removed");
        }
        const fondo = document.querySelector('.vsc-initialized');
        if (fondo) {
            fondo.style.height = isEpisodePage() ? '0%' : '100%';
            debugEnable && console.log("[mainLogic]: HIDE_PREMIUM_TRIAL: Player background adjusted");
        }
    }
    // Verifies conditions to censor tooltips
    const targetToolTip = document.querySelector('.app-body-wrapper');
    if (USER_CONFIG.MODIFY_TOOLTIPS) {
        debugEnable && onsole.log("[mainLogic-censorToolTips]: USER_CONFIG.MODIFY_TOOLTIPS is enabled.");
        
        if (targetToolTip) {
            debugEnable && console.log("[mainLogic-censorToolTips]: Target tooltip general element (.app-body-wrapper) found.");
            
            if (!isWatchlistPage()) {
                debugEnable && console.log("[mainLogic-censorToolTips]: Not on the watchlist page. Censoring tooltips.");
                censorTooltips();
            } else {
                debugEnable && console.log("[mainLogic-censorToolTips]: On the watchlist page. Skipping tooltip censorship.");
            }
        } else {
            debugEnable && console.log("[mainLogic-censorToolTips]: Target tooltip general element (.app-body-wrapper) not found.");
        }
    } else {
        debugEnable && console.log("[mainLogic-censorToolTips]: USER_CONFIG.MODIFY_TOOLTIPS is not enabled.");
    }
    const targetDocTitle = document.querySelector('head > title');
    // In episode pages operations
    if (isEpisodePage()) {
        debugEnable && console.log("[mainLogic-EP Page exlusive]: On episode page.");
        // Verifies conditions to censor document title (browser's tab) (just on episode pages)
        if (USER_CONFIG.MODIFY_DOCTITLE_EPISODE_TITLE) {
            debugEnable && console.log("[mainLogic-censorDocTitle]: USER_CONFIG.MODIFY_DOCTITLE_EPISODE_TITLE is ON.");
            if (targetDocTitle) {
                debugEnable && console.log("[mainLogic-censorDocTitle]: Modifying document title (browser's tab).");
                censorDocTitle();
            } else {
                debugEnable && console.log("[mainLogic-censorDocTitle]: Document title element not found.");
            }
        } else {
            debugEnable && console.log("[mainLogic-censorDocTitle]: USER_CONFIG.MODIFY_DOCTITLE_EPISODE_TITLE is OFF.");
        }
        // Verifies conditions to censor URL's (just on episode pages)
        if (USER_CONFIG.MODIFY_URL_EPISODE_TITLE) {
            debugEnable && console.log("[mainLogic-censorURL]: USER_CONFIG.MODIFY_URL_EPISODE_TITLE is ON.");
            if (targetDocTitle) {
                debugEnable && console.log("[mainLogic-censorURL]: Modifying URL.");
                censorUrl();
            } else {
                debugEnable && console.log("[mainLogic-censorURL]: Document URL general element (title) not found.");
            }
        } else {
            debugEnable && console.log("[mainLogic-censorURL]: USER_CONFIG.MODIFY_URL_EPISODE_TITLE is OFF.");
        }

    } else {
        debugEnable && console.log("[mainLogic-EP Page exlusive]: Not on episode page.");
    }
    // Verifies conditions to censor episode titles on whatever page is needed. 
    // modifyActive controls if the title should be censored or not to have a more flexible control (advanced)
    if (USER_CONFIG.MODIFY_INSITE_EPISODE_TITLES) {
        debugEnable && console.log("[mainLogic-censorTitleGeneric]: USER_CONFIG.MODIFY_INSITE_EPISODE_TITLES is enabled.");
        for (let key in cssSelectorList["TITLES"]) {
            const config = cssSelectorList["TITLES"][key];
            if (config["modifyActive"]) {
                const selectorString = config["selector"];
                const targetPlayerTitle = document.querySelector(selectorString);
    
                if (targetPlayerTitle) {
                    debugEnable && console.log(`[mainLogic-censorTitleGeneric]: Censoring title for selector: ${selectorString}`);
                    censorTitleGeneric(selectorString);
                } else {
                    debugEnable && console.log(`[mainLogic-censorTitleGeneric]: Target element not found for selector: ${selectorString}`);
                }
            } else {
                debugEnable && console.log(`[mainLogic-censorTitleGeneric]: Modification not active for key: ${key}`);
            }
        }
    } else {
        debugEnable && console.log("[mainLogic-censorTitleGeneric]: USER_CONFIG.MODIFY_INSITE_EPISODE_TITLES is not enabled.");
    }
    
    debugEnable && console.log("[mainLogic]: END");
}

// ----------------------------- v1.2.0 -----------------------------
function extractEpisodeNumber(text) {
    // Regexp to find the episode number after 'E' (ignores possible season number 'S')
    const match = text.match(/(?:S\d+\s*)?E(\d+)/);
    // If there's a match, return the episode number parsed as an integer
    if (match) {
        return parseInt(match[1], 10);
    }
    // If not, return NaN
    return NaN;
}
function timeToSeconds(time) {
    const [minutes, secondsWithMillis] = time.split(':').map(Number);
    return minutes * 60 + secondsWithMillis;
}
function loadJSON() {
    if (USER_CONFIG.FETCH_INSTEAD_OF_RESOURCE) {
        fetch('https://raw.githubusercontent.com/zAlfok/ByeSpoilers-Crunchyroll/master/scripts/crunchyroll_titles_intervals_compactSimplified.json')
            .then(response => {
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                return response.json();
            })
            .then(data => {
                titleIntervals = data; // Asigna los datos a la variable global
                console.log('Data loaded successfully:', titleIntervals);
            })
            .catch(error => console.error('Error loading JSON:', error));
    } else {
        try {
            const jsonText = GM_getResourceText("TITLE_INTERVALS_JSON");
            titleIntervals = JSON.parse(jsonText);
            debugEnable && console.log("[loadJSON]: Title intervals loaded:", titleIntervals);
        } catch (error) {
            console.error("[loadJSON]: Error loading title intervals:", error, "\nTry to set FETCH_INSTEAD_OF_RESOURCE to true in the USER_CONFIG section.\nTrying to fetch the JSON file instead.");
            fetch('https://raw.githubusercontent.com/zAlfok/ByeSpoilers-Crunchyroll/master/scripts/crunchyroll_titles_intervals_compactSimplified.json')
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Network response was not ok');
                    }
                    return response.json();
                })
                .then(data => {
                    titleIntervals = data; // Asigna los datos a la variable global
                    console.log('Data loaded successfully:', titleIntervals);
                })
                .catch(error => console.error('Error loading JSON:', error));
        }
    }
    }

function initializeMainPage() {
    // Listens to messages from the player iframe
    window.addEventListener('message', function(event) {
        if (event.origin !== "https://static.crunchyroll.com") return;
        debugEnable && console.log("[initializeMainPage]: Main page received message:", event.data);
        
        // If the message contains the current time of the player do the following
        if (event.data.currentTime !== undefined) {
            const iframe = document.querySelector('iframe[src^="https://static.crunchyroll.com"]');
            if (!iframe) {
                debugEnable && console.log("[initializeMainPage]: Player iframe not found");
                return;
            }
            const currentTime = event.data.currentTime;
            debugEnable && console.log("[initializeMainPage]: Current time:", currentTime);
            
            [episodeNumberStr, episodeTitle, seriesName] = getEpisodeTitleFromEpisodeSite();
            episodeNumberInt = extractEpisodeNumber(episodeNumberStr);
            if (titleIntervals[seriesName] && titleIntervals[seriesName][`${episodeNumberInt}`]) {
                const interval = titleIntervals[seriesName][`${episodeNumberInt}`];
                const startTime = timeToSeconds(interval[0]);
                const endTime = timeToSeconds(interval[1]);
                // If current time is within the interval, skip it
                if (currentTime >= startTime-0.5 && currentTime <= endTime+0.5) {

                    debugEnable && console.log("[initializeMainPage]: Skipping interval");
                    // If iframe is found, send a message to the player to skip the interval
                    iframe.contentWindow.postMessage({action: 'setCurrentTime', time: endTime+0.5}, '*');

                } 
            }

        }
    });

    // Ask for the player's current time, every second
    setInterval(function() {
        const iframe = document.querySelector('iframe[src^="https://static.crunchyroll.com"]');
        if (iframe) {
            debugEnable && console.log("[initializeMainPage]: Sending getCurrentTime message (1s interval)");
            iframe.contentWindow.postMessage({action: 'getCurrentTime'}, 'https://static.crunchyroll.com');
        }
    }, 500);
}

function initializePlayerIframe() {
    // Listens to messages from the main page
    window.addEventListener('message', function(event) {
        if (event.origin !== "https://www.crunchyroll.com") return;
        debugEnable && console.log("[initializePlayerIframe]: Player iframe received message:", event.data);
        // Searches for video player
        const player = document.querySelector('video');
        if (!player) {
            debugEnable && console.log("[initializePlayerIframe]: Video player not found in iframe");
            return;
        }
        // Handle received messages
        if (event.data.action === 'getCurrentTime') {
            debugEnable && console.log("[initializePlayerIframe]: Getting current time:", player.currentTime);
            window.parent.postMessage({currentTime: player.currentTime}, 'https://www.crunchyroll.com');
        } else if (event.data.action === 'setCurrentTime') {
            debugEnable && console.log("[initializePlayerIframe]: Setting current time to:", event.data.time);
            player.currentTime = event.data.time;
        }
    });
}

// Execution
try {
    console.log('[Bye Spoilers - Crunchyroll]: Script execution started');
    if (window.location.hostname === "www.crunchyroll.com") {
        console.log("Script running on main Crunchyroll page");
        loadJSON();
        // Blur the page while DOM and script are loading
        document.documentElement.style.filter = 'blur(8px)';
        debugEnable && console.log("[Bye Spoilers - Crunchyroll]: First load blur applied.");
        // Apply cssE style to the page (hidePremiumTrial is not applied here completely, part is done on mainLogic)
        try {
            concatStyleCSS();
            var $newStyleE = document.createElement('style');
            var cssNodeE = document.createTextNode(cssE);
            $newStyleE.appendChild(cssNodeE);
            document.head.appendChild($newStyleE);
            debugEnable && console.log('[ByeSpoilers - Crunchyroll Script]: CSS Applied');
        } catch (e) {
            debugEnable && console.error('[ByeSpoilers - Crunchyroll Script] DEBUG: CSS Error:', e);
        }
        // When the page is loaded, apply the main logic and set a MutationObserver to 
        // apply censorship again when the DOM changes (because of SPA behavior)
        window.addEventListener('load', function () {
            debugEnable && console.log("[Bye Spoilers - Crunchyroll]: Window loaded, executing mainLogic after 0ms timeout");
            setTimeout(mainLogic(),0);
            debugEnable && console.log("[Bye Spoilers - Crunchyroll]: MutationObserver set to apply censorship again when the DOM changes");
            new MutationObserver(() => {
                debugEnable && console.log("[Bye Spoilers - Crunchyroll]: MutationObserver triggered, executing mainLogic");
                mainLogic();
            }).observe(document, { subtree: true, childList: true });
        });

        USER_CONFIG.SKIP_EPISODE_TITLES && initializeMainPage();

    } else if (window.location.hostname === "static.crunchyroll.com") {
        console.log("Script running in video player iframe");
        USER_CONFIG.SKIP_EPISODE_TITLES && initializePlayerIframe();
    }
    console.log('[Bye Spoilers - Crunchyroll]: Script execution finished. Observer keeping track of changes.');

} catch (e) {
    console.error('[Bye Spoilers - Crunchyroll]: There was an error loading the script. If this causes noticeable issues, please leave feedback including this error:', e);
    throw e;
}