netaware / Cruncyroll Hard Subs > No Subs

// ==UserScript==
// @name         Cruncyroll Hard Subs > No Subs
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Use hard subs if soft subs are not availible
// @author       Netaware
// @license      MIT
// @match        https://www.crunchyroll.com/*
// @grant        unsafeWindow
// ==/UserScript==

(function() {
    'use strict';

    var changedVilosPlayer = false;
    var origTokenList = DOMTokenList.prototype.add;
    var origVilosPlayer;

    // This is a setter function.
    // It reads the config, and if no ${lang} soft subs are availible,
    // it deletes the soft subs config, forcing the player to use hard subs.
    function scanConfig(config) {
      try {
        var lang= this.player.language;
        var soft_subs_lang = config.subtitles.map(sub => sub.language);
        if(!soft_subs_lang.includes(lang)) {
            console.log('[hs > ns] deleting soft subtitles');
            delete config.subtitles;
        };
      } catch (error) {
        console.log('[hs > ns] had trouble reading config');
        console.log(error);
      };
      // set "vilos.config.media" to its real (possibly modified) value.
      Object.defineProperty(this, 'media', {value:config});
    };

    unsafeWindow.DOMTokenList.prototype.add = function(item) {
        // Is "VilosPlayer" defined and have we already made our changes?
        if(unsafeWindow.VilosPlayer !== undefined && !changedVilosPlayer) {
            changedVilosPlayer = true;
            origVilosPlayer = unsafeWindow.VilosPlayer;
            // wrap "VilosPlayer" to set "vilos.config.media" to a setter function
            unsafeWindow.VilosPlayer = function() {
                origVilosPlayer.call(this,...arguments);
                Object.defineProperty(this.config, 'media',
                                      {set: scanConfig});
            };
        }
        return origTokenList.call(this, ...arguments);
    };
})();

// Plan of attack:
// If the video player is given a configuration which has
// hard subs and soft subs, then the player will only play soft subs.
// This is the case even if the customer's perfered language is $LANG,
// there are hard subs in $LANG, and no soft subs in $LANG.
// This prevents the customer from viewing subs in $LANG even
// if such subs are availible as hard subs.
// Therefore we need to intercept the configuration before it is passed
// to the video player, check if soft subs aren't availible in $LANG,
// and delete soft subs from the configuration if so.

// The relevent bits of Crunchyroll's code is as follows:

// <script src=".../vilosplayer.js">
// //inside vilosplayer.js
// function VilosPlayer(...) { ... } // Constructs a vilos player object
// </script>
// <script>
// (function () { // anon function all the stuff we care about happens in
//   function buildPlayer(...) {
//     var vilos = new VilosPlayer();
//     vilos.config.player.language = ...;  // the customer's preferred language
//     ...
//     vilos.config.media = { ... };  // where the data we want to read/modify is
//     ...
//   }
//
//   function setStylingWideAspectRatio() {
//     ...
//     (...).classList.add(...) // our chance to change global scope
//     ...
//   }
//   ...
//   document.addEventListener(..., function(e) {
//     setStylingWideAspectRatio(); // change VilosPlayer here
//     var vilosPlayer = buildPlayer(...); // need to change VilosPlayer before this is called
//     vilosPlayer.load(...); // we want to read and change the config before here
//   });
// }());
// </script>

// So we want to read and maybe modify "vilos.config.media"
// before "vilosPlayer.load" is called.
// Since all this is encapsulated in an anon function,
// the only way we can do this is to change the global scope.

// Changing the "VilosPlayer" constructor to make "vilos.config.media"
// a setter function should do exactly what we want.
// Unfortunatly, this script runs before "VilosPlayer" is defined.
// If we try to change "window.VilosPlayer" to a setter,
// we get thrown an error because VilosPlayer uses function declaration syntax.
// HOWEVER, the "SetStylingWideAspectRatio" function calls
// (...).classList.add(...), which we can override by setting
// "DOMTokenList.prototype.add".

// So in conclusion, we change the "DOMTokenList.prototype.add"
// so that it checks if "VilosPlayer" is defined when it is called.
// If it is, then "VilosPlayer" is wrapped in a function which
// makes "vilos.config.media" a setter.
// This allows us to read and change the value of "vilos.config.media" when it is defined.

// Yea, I know this is brittle. I can't think of a better way though.