Greasy Fork is available in English.

CrunchyUtils

Adds a none option to the subtitle picker, more playback speeds, faster UI autohide, custom fast-forward and rewind values, and frame seeking!

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         CrunchyUtils
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds a none option to the subtitle picker, more playback speeds, faster UI autohide, custom fast-forward and rewind values, and frame seeking!
// @author       DimitrovN
// @match        https://www.crunchyroll.com/*
// @run-at       document-start
// @noframes
// @grant        none
// @license      GPL-3.0-or-later
// @icon         https://www.crunchyroll.com/build/assets/img/favicons/favicon-v2-32x32.png
// ==/UserScript==

/* eslint-disable */

(function () {
  'use strict';

  // ============================================================
  //  FEATURE FLAGS  -  set to false to disable a feature
  // ============================================================
  const FEATURE_SUBTITLE_NONE    = true;   // Adds "None" option + deselect in subtitle menu
  const FEATURE_PLAYBACK_SPEED   = true;   // Adds extra playback speeds (3x, 2.5x, 2x, 1.75x, 1.5x, 1.25x)
  const FEATURE_AUTOHIDE         = true;   // Reduces UI autohide delay
  const FEATURE_SEEK_BUTTONS     = true;   // Changes skip buttons to SEEK_SECONDS
  const FEATURE_ARROW_KEY_SEEK   = true;   // Arrow keys seek by SEEK_SECONDS
  const FEATURE_FRAME_STEP       = true;   // , and . for frame-by-frame step

  // ============================================================
  //  SETTINGS
  // ============================================================
  const HIDE_DELAY_MS  = 500;                            // Autohide delay in milliseconds (default CR: 6000)
  const SEEK_SECONDS   = 5;                              // Seconds to fast-forward and rewind (seek) with buttons/arrow keys
  const FRAME_SECONDS  = 1 / 23.976;                     // Seconds per frame (adjust for 24/25/30 fps content)
  const EXTRA_SPEEDS   = [3, 2.5, 2, 1.75, 1.5, 1.25];   // Speeds to inject (highest first)

  // ============================================================
  //  SHARED UTILITIES
  // ============================================================
  const CHECKMARK_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="color:white;">
    <path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM15.7929 8.29289L17.2071 9.70711L10.5 16.4142L6.79289 12.7071L8.20711 11.2929L10.5 13.5858L15.7929 8.29289Z" fill="currentColor"></path>
  </svg>`;

  function getVideo() {
    return document.querySelector('video');
  }

  function getBitmovinPlayer() {
    const video = document.querySelector('video[id^="bitmovinplayer-video"]');
    return video?.parentElement?.player ?? null;
  }

  // ============================================================
  //  FEATURE: AUTOHIDE
  // ============================================================
  if (FEATURE_AUTOHIDE) {
    const _st = window.setTimeout;
    window.setTimeout = function (fn, delay, ...args) {
      if (delay === 6000 && fn?.toString?.() === '()=>{s(!1)}') {
        delay = HIDE_DELAY_MS;
      }
      return _st.call(window, fn, delay, ...args);
    };
  }

  // ============================================================
  //  FEATURE: SEEK BUTTONS + ARROW KEYS + FRAME STEP
  // ============================================================
  if (FEATURE_SEEK_BUTTONS || FEATURE_ARROW_KEY_SEEK || FEATURE_FRAME_STEP) {

    function seek(seconds) {
      const player = getBitmovinPlayer();
      if (!player) return;
      player.seek(player.getCurrentTime() + seconds);
    }

    if (FEATURE_SEEK_BUTTONS) {
      function attachSeekButtons() {
        const backward = document.querySelector('[data-testid="jump-backward-button"]');
        const forward  = document.querySelector('[data-testid="jump-forward-button"]');
        if (backward) {
          backward.addEventListener('click', e => {
            e.stopImmediatePropagation();
            seek(-SEEK_SECONDS);
          }, true);
        }
        if (forward) {
          forward.addEventListener('click', e => {
            e.stopImmediatePropagation();
            seek(SEEK_SECONDS);
          }, true);
        }
      }

      const seekBtnObserver = new MutationObserver(() => {
        const backward = document.querySelector('[data-testid="jump-backward-button"]');
        if (backward) {
          seekBtnObserver.disconnect();
          attachSeekButtons();
        }
      });
      document.addEventListener('DOMContentLoaded', () => {
        seekBtnObserver.observe(document.body, { childList: true, subtree: true });
      });
    }

    if (FEATURE_ARROW_KEY_SEEK) {
      document.addEventListener('keydown', e => {
        if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
          if (!getBitmovinPlayer()) return;
          e.stopImmediatePropagation();
          seek(e.key === 'ArrowLeft' ? -SEEK_SECONDS : SEEK_SECONDS);
        }
      }, true);
    }

    if (FEATURE_FRAME_STEP) {
      document.addEventListener('keydown', e => {
        if (e.key === ',' || e.key === '.') {
          const player = getBitmovinPlayer();
          if (!player) return;
          e.preventDefault();
          e.stopImmediatePropagation();
          if (!player.isPaused()) player.pause();
          seek(e.key === ',' ? -FRAME_SECONDS : FRAME_SECONDS);
        }
      }, true);
    }
  }

  // ============================================================
  //  FEATURE: SUBTITLE NONE / DESELECT  +  PLAYBACK SPEED
  //  (deferred until DOM is ready, since these need document.body)
  // ============================================================
  function initDOMFeatures() {

  if (FEATURE_SUBTITLE_NONE) {

    let bearerToken  = null;
    let noneIsActive = false;

    // - Subtitle preference persistence setup -
    const origFetch = window.fetch;
    window.fetch = async function (...args) {
      try {
        const opts    = args[1] || {};
        const headers = opts.headers || {};
        const auth    = headers instanceof Headers
          ? headers.get('authorization')
          : (headers['authorization'] || headers['Authorization'] || '');
        if (auth && auth.startsWith('Bearer ')) bearerToken = auth.slice(7);
      } catch (e) {}
      return origFetch.apply(this, args);
    };

    const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;
    XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
      if (header.toLowerCase() === 'authorization' && value.startsWith('Bearer ')) {
        bearerToken = value.slice(7);
      }
      return origSetHeader.apply(this, arguments);
    };

    function getProfileId() {
      const match = document.cookie.match(/ajs_user_id=([a-f0-9-]+)/);
      return match ? match[1] : null;
    }

    function getTracksOrchestrator() {
      const el  = document.querySelector('.player-container');
      if (!el) return null;
      const key = Object.keys(el).find(k => k.startsWith('__reactFiber'));
      if (!key) return null;
      const comp = el[key].return?.stateNode;
      return comp?.player?._katamariPlayer?.playerOrchestrator?.tracksOrchestrator || null;
    }

    function closeSubtitleMenu() {
      document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
      document.body.click();
    }

    function switchToCleanStream() {
      const tracks = getTracksOrchestrator();
      if (!tracks) return false;
      const available = tracks._availableTextTracks;
      if (!available?.length) return false;
      const existingUrl = available[0].videoUrl?.href || available[0].videoUrl;
      if (!existingUrl) return false;
      const cleanUrl = existingUrl.replace(/\/\d+\/[^/]+\/(dash\/manifest\.mpd)/, '/1/clean/$1');
      let cleanTrack = available.find(t => t.language === 'off');
      if (!cleanTrack) {
        cleanTrack = { role: 2, format: 2, language: 'off', displayName: 'None', videoUrl: new URL(cleanUrl) };
        available.push(cleanTrack);
      }
      closeSubtitleMenu();
      setTimeout(() => {
        tracks.setTextTrack(cleanTrack);
        console.log('[CR-None] Switched to clean stream');
      }, 100);
      return true;
    }

    function switchToSubtitleStream(locale) {
      const tracks = getTracksOrchestrator();
      if (!tracks) return false;
      const track = tracks._availableTextTracks.find(t => t.language === locale);
      if (!track) return false;
      closeSubtitleMenu();
      setTimeout(() => {
        tracks.setTextTrack(track);
        console.log('[CR-None] Switched to:', locale);
      }, 100);
      return true;
    }

    async function persistSubtitlePreference(lang) {
      const pid = getProfileId();
      if (!pid || !bearerToken) return;
      try {
        await origFetch(`https://www.crunchyroll.com/accounts/v2/me/multiprofile/${pid}`, {
          method: 'PATCH',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${bearerToken}`,
          },
          body: JSON.stringify({ preferred_content_subtitle_language: lang }),
        });
        console.log('[CR-None] Persisted:', lang);
      } catch (e) {}
    }

    const langMap = {
      'English': 'en-US', 'Deutsch': 'de-DE',
      'Español (América Latina)': 'es-419', 'Español (España)': 'es-ES',
      'Français': 'fr-FR', 'Italiano': 'it-IT',
      'Português (Brasil)': 'pt-BR', 'Русский': 'ru-RU', 'العربية': 'ar-SA',
    };

    function clearSubtitleItems(container) {
      container.querySelectorAll('[role="menuitemradio"]').forEach(item => {
        item.setAttribute('aria-checked', 'false');
        item.classList.remove('kat:bg-white/6');
        const icon = item.querySelector('div:last-child');
        if (icon) icon.innerHTML = '';
      });
    }

    function injectNoneOption(container) {
      if (container.querySelector('[data-none-option]')) return;

      // Find the scrollable container with the menu items
      const scrollableContainer = container.querySelector('.kat\\:overflow-y-auto') || container;
      
      // Remove any Crunchyroll-created "None" button (without our custom marker)
      const noneButtons = Array.from(scrollableContainer.querySelectorAll('[role="menuitemradio"]')).filter(el => 
        el.getAttribute('aria-label') === 'None' && !el.hasAttribute('data-none-option')
      );
      noneButtons.forEach(btn => btn.remove());
      
      const firstItem = scrollableContainer.querySelector('[role="menuitemradio"]');
      if (!firstItem) return;

      const noneItem = document.createElement('div');
      noneItem.setAttribute('data-none-option', 'true');
      noneItem.setAttribute('role', 'menuitemradio');
      noneItem.setAttribute('aria-label', 'None');
      noneItem.setAttribute('aria-checked', 'false');
      noneItem.className = 'kat:flex kat:items-center kat:gap-4 kat:cursor-pointer kat:transition-colors kat:ps-20 kat:pe-20 kat:pt-13 kat:pb-13 kat:hover:bg-neutral-600 kat:focus-visible:outline-4 kat:focus-visible:-outline-offset-4 kat:focus-visible:outline-orange-500 kat:focus-visible:bg-neutral-600 kat:active:bg-neutral-500';
      noneItem.innerHTML = `
        <div class="kat:flex kat:flex-col kat:flex-1 kat:min-w-0 kat:gap-2 kat:text-start">
          <span class="kat:text-sm kat:text-neutral-50">None (Custom)</span>
        </div>
        <div class="kat:w-24 kat:h-24 kat:shrink-0 cr-none-check"></div>
      `;

      try {
        scrollableContainer.insertBefore(noneItem, firstItem);
      } catch (e) {
        scrollableContainer.appendChild(noneItem);
      }

      function setNoneActive(active) {
        noneIsActive = active;
        noneItem.setAttribute('aria-checked', active ? 'true' : 'false');
        noneItem.classList.toggle('kat:bg-white/6', active);
        noneItem.querySelector('.cr-none-check').innerHTML = active ? CHECKMARK_SVG : '';
      }

      if (noneIsActive) {
        setNoneActive(true);
        clearSubtitleItems(scrollableContainer);
        noneItem.setAttribute('aria-checked', 'true');
        noneItem.classList.add('kat:bg-white/6');
        noneItem.querySelector('.cr-none-check').innerHTML = CHECKMARK_SVG;
      }

      noneItem.addEventListener('click', () => {
        clearSubtitleItems(scrollableContainer);
        setNoneActive(true);
        switchToCleanStream();
        persistSubtitlePreference('off');
      });

      scrollableContainer.addEventListener('click', (e) => {
        const clicked = e.target.closest('[role="menuitemradio"]');
        if (!clicked || clicked === noneItem) return;
        const wasChecked = clicked.getAttribute('aria-checked') === 'true';

        clearSubtitleItems(scrollableContainer);
        setNoneActive(false);

        if (!wasChecked) {
          clicked.setAttribute('aria-checked', 'true');
          clicked.classList.add('kat:bg-white/6');
          const icon = clicked.querySelector('div:last-child');
          if (icon) icon.innerHTML = CHECKMARK_SVG;
          const locale = langMap[clicked.getAttribute('aria-label')] || clicked.getAttribute('aria-label');
          switchToSubtitleStream(locale);
          persistSubtitlePreference(locale);
        } else {
          setNoneActive(true);
          switchToCleanStream();
          persistSubtitlePreference('off');
        }
      }, { capture: true });
    }

    let subtitleDebounce = null;
    const subtitleObserver = new MutationObserver(() => {
      clearTimeout(subtitleDebounce);
      subtitleDebounce = setTimeout(() => {
        // Find the subtitle menu by looking for the "Subtitles/CC" header
        const allMenus = document.querySelectorAll('[role="menu"]');
        
        allMenus.forEach((menu) => {
          // Look for a separator with "Subtitles/CC" text inside this menu's parent
          const parent = menu.parentElement;
          const header = parent?.querySelector('[role="separator"]');
          
          if (header && header.textContent.includes('Subtitles/CC')) {
            injectNoneOption(menu);
          }
        });
      }, 50);
    });

    subtitleObserver.observe(document.body, { childList: true, subtree: true });
  }

  // ============================================================
  //  FEATURE: PLAYBACK SPEED
  // ============================================================
  if (FEATURE_PLAYBACK_SPEED) {

    const SPEED_ITEM_CLASSES = [
      'kat:flex', 'kat:items-center', 'kat:gap-4', 'kat:cursor-pointer',
      'kat:transition-colors', 'kat:ps-20', 'kat:pe-20', 'kat:pt-13', 'kat:pb-13',
      'kat:hover:bg-neutral-600', 'kat:focus-visible:outline-4',
      'kat:focus-visible:-outline-offset-4', 'kat:focus-visible:outline-orange-500',
      'kat:focus-visible:bg-neutral-600', 'kat:active:bg-neutral-500',
    ].join(' ');

    let savedSpeed = 1;

    function updateSpeedButton(speed) {
      const btn = document.querySelector('[data-testid="playback-speed-button"]');
      if (btn) btn.textContent = `${speed}x`;
    }

    function updateCheckedState(menu, activeSpeed) {
      if (!menu) return;
      menu.querySelectorAll('[role="menuitemradio"]').forEach(el => {
        const speed    = parseFloat(el.getAttribute('aria-label'));
        const isActive = speed === activeSpeed;
        el.setAttribute('aria-checked', isActive ? 'true' : 'false');
        el.classList.toggle('kat:bg-white/6', isActive);
        const checkDiv = el.querySelector('div:last-child');
        if (checkDiv) checkDiv.innerHTML = isActive ? CHECKMARK_SVG : '';
      });
    }

    function createSpeedItem(speed) {
      const item = document.createElement('div');
      item.role = 'menuitemradio';
      item.setAttribute('aria-label', `${speed}x`);
      item.setAttribute('aria-checked', 'false');
      item.setAttribute('aria-disabled', 'false');
      item.setAttribute('tabindex', '0');
      item.className = SPEED_ITEM_CLASSES;
      item.dataset.crSpeed = speed;
      item.innerHTML = `
        <div class="kat:flex kat:flex-col kat:flex-1 kat:min-w-0 kat:gap-2 kat:text-start">
          <span class="kat:text-sm kat:text-neutral-50">${speed}x</span>
        </div>
        <div class="kat:w-24 kat:h-24 kat:shrink-0 cr-speed-check"></div>
      `;
      item.addEventListener('click', () => {
        savedSpeed = speed;
        const video = getVideo();
        if (video) video.playbackRate = speed;
        updateCheckedState(item.closest('[role="menu"]'), speed);
        updateSpeedButton(speed);
      });
      item.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          item.click();
        }
      });
      return item;
    }

    function attachVideoListeners(video) {
      if (video.dataset.crSpeedListening) return;
      video.dataset.crSpeedListening = 'true';

      video.addEventListener('play', () => {
        if (video.playbackRate !== savedSpeed) video.playbackRate = savedSpeed;
      });

      video.addEventListener('ratechange', () => {
        const menu = document.querySelector('[data-testid="playback-speed-menu"] [role="menu"]');
        updateCheckedState(menu, video.playbackRate);
        updateSpeedButton(video.playbackRate);
        if (video.playbackRate !== savedSpeed && !video.dataset.crSpeedRestoring) {
          video.dataset.crSpeedRestoring = 'true';
          video.playbackRate = savedSpeed;
          delete video.dataset.crSpeedRestoring;
        }
      });
    }

    function injectSpeeds(menu) {
      if (menu.dataset.crSpeedInjected) return;
      menu.dataset.crSpeedInjected = 'true';

      const existingItems    = menu.querySelectorAll('[role="menuitemradio"]');
      const lastExistingItem = existingItems[existingItems.length - 1];

      existingItems.forEach(el => {
        el.addEventListener('click', () => {
          savedSpeed = parseFloat(el.getAttribute('aria-label'));
          updateSpeedButton(savedSpeed);
        });
      });

      EXTRA_SPEEDS.forEach(speed => {
        const item = createSpeedItem(speed);
        if (lastExistingItem) lastExistingItem.after(item);
        else menu.appendChild(item);
      });

      const video = getVideo();
      if (video) {
        attachVideoListeners(video);
        updateCheckedState(menu, video.playbackRate);
        updateSpeedButton(video.playbackRate);
      }
    }

    const speedObserver = new MutationObserver(() => {
      const speedMenu = document.querySelector('[data-testid="playback-speed-menu"] [role="menu"]');
      if (speedMenu && !speedMenu.dataset.crSpeedInjected) injectSpeeds(speedMenu);
    });

    speedObserver.observe(document.body, { childList: true, subtree: true });
  }

  } // end initDOMFeatures

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initDOMFeatures);
  } else {
    initDOMFeatures();
  }

})();