Harest / Twitch - Mute ads and optionally hide them

// ==UserScript==
// @name        Twitch - Mute ads and optionally hide them
// @namespace   TWITCHADS
// @description Automatically mutes the Twitch player when an advertisement started and unmute it once finished. You can also hide ads by setting disableDisplay to true.
// @include     https://www.twitch.tv/*
// @include     https://twitch.tv/*
// @version     1.1453
// @license     MIT
// @author      Harest
// @grant       none
// ==/UserScript==
(function() {
  var _tmuteVars = { "timerCheck": 1000, // Checking rate of ad in progress (in ms ; EDITABLE)
                    "playerMuted": false, // Player muted or not (due to ad in progress)
                    "adsDisplayed": 0, // Number of ads displayed
                    "disableDisplay": false, // Disable the player display during an ad (true = yes, false = no (default) ; EDITABLE)
                    "alreadyMuted": false, // Used to check if the player is muted at the start of an ad
                    "adElapsedTime": undefined, // Used to check if Twitch forgot to remove the ad notice
                    "adUnlockAt": 210, // Unlock the player if this amount of seconds elapsed during an ad (EDITABLE)
                    "adMinTime": 7, // Minimum amount of seconds the player will be muted/hidden since an ad started (EDITABLE)
                    "squadPage": false, // Either the current page is a squad page or not
                    "playerIdAds": 0, // Player ID where ads may be displayed (default 0, varying on squads page)
                    "displayingOptions": false, // Either ads options are currently displayed or not
                    "highwindPlayer": undefined, // If you've the Highwind Player or not
                    "classFallback": false, // If we're in a browser without the main ad notice class
                    "currentPage": undefined // Current page to know if we need to reset ad detection on init
                   };
  
  // Selectors for the old player and the highwind one
  var _tmuteSelectors = { "old": { "player": "player-video", // Player class
                                   "playerVideo": ".player-video", // Player video selector
                                   "muteButton": ".player-button--volume", // (un)mute button selector
                                   "adNotice": "player-ad-notice", // Ad notice class
                                   "adNoticeFallback": "player-ad-notice", // Ad notice fallback class as the main one seems missing in at least Chrome
                                   "viewersCount": "channel-info-bar__viewers-wrapper", // Viewers count wrapper class
                                   "squadHeader": "squad-stream-top-bar__container", // Squad bar container class
                                   "squadPlayer": "multi-stream-player-layout__player-container", // Squad player class
                                   "squadPlayerMain": "multi-stream-player-layout__player-primary" // Squad primary player class
  																},
                          "hw":  { "player": "video-player__container", // Player class
                                   "playerVideo": ".video-player__container video", // Player video selector
                                   "muteButton": "button[data-a-target='player-mute-unmute-button']", // (un)mute button selector
                                   "adNotice": "Layout-sc-nxg1ff-0 fpnJwy", // Ad notice class
                                   "adNoticeFallback": "fpnJwy", // Ad notice fallback class as the main one seems missing in at least Chrome
                                   "viewersCount": "metadata-layout__support", // Viewers count wrapper class
                                   "squadHeader": "squad-stream-top-bar__container", // Squad bar container class
                                   "squadPlayer": "multi-stream-player-layout__player-container", // Squad player class
                                   "squadPlayerMain": "multi-stream-player-layout__player-primary" // Squad primary player class
                          				}
  											};
	// Current selector (either old or highwind player, automatically set below)
  var currentSelector = undefined;
	
  // Check if there's an ad
  function checkAd()
  { 
    // Check if you're watching a stream, useless to continue if not
    if (_tmuteVars.highwindPlayer === undefined) {
      var isOldPlayer = document.getElementsByClassName(_tmuteSelectors.old.player).length;
      var isHwPlayer = document.getElementsByClassName(_tmuteSelectors.hw.player).length;
      var isViewing = Boolean(isOldPlayer + isHwPlayer);
      if (isViewing === false) return;
      
      // We set the type of player currently used (old or highwind one)
      _tmuteVars.highwindPlayer = Boolean(isHwPlayer);
      currentSelector = (_tmuteVars.highwindPlayer === true) ? _tmuteSelectors.hw : _tmuteSelectors.old;
      console.log("You're currently using the " + ((_tmuteVars.highwindPlayer === true) ? "Highwind" : "old") + " player.");
    } else {
      var isViewing = Boolean(document.getElementsByClassName(currentSelector.player).length);
      if (isViewing === false) return;
    }
    
    // Initialize the ads options if necessary.
    var optionsInitialized = (document.getElementById("_tmads_options") === null) ? false : true;
    if (optionsInitialized === false) adsOptions("init");
    
    var selectorId = _tmuteVars.playerIdAds * 2;
    var advert = document.getElementsByClassName(currentSelector.adNotice)[selectorId];
    
    if (_tmuteVars.adElapsedTime !== undefined)
    {
      _tmuteVars.adElapsedTime += _tmuteVars.timerCheck / 1000;
      if (_tmuteVars.adElapsedTime >= _tmuteVars.adUnlockAt && advert.childNodes[1] !== undefined) 
      {
        for (var i = 0; i < advert.childElementCount; i++)
        {
          if (!advert.childNodes[i].classList.contains(currentSelector.adNotice)) advert.removeChild(advert.childNodes[i]);
        }
        console.log("Unlocking Twitch player as Twitch forgot to remove the ad notice after the ad(s).");
      }
    }
    
    if ((advert.childElementCount > 2 && _tmuteVars.playerMuted === false) || (_tmuteVars.playerMuted === true && advert.childElementCount <= 2)) 
    {
      // Update at the start of an ad if the player is already muted or not
      if (advert.childElementCount > 2) {
        var muteButton = document.querySelectorAll(currentSelector.muteButton)[_tmuteVars.playerIdAds];
        if (_tmuteVars.highwindPlayer === true) {
        	_tmuteVars.alreadyMuted = Boolean(muteButton.getAttribute("aria-label") === "Unmute (m)");
        } else {
        	_tmuteVars.alreadyMuted = Boolean(muteButton.childNodes[0].className === "unmute-button");
        }
      }
      
      // Keep the player muted/hidden for the minimum ad time set (Twitch started to remove the ad notice before the end of some ads)
      if (advert.childElementCount <= 2 && _tmuteVars.adElapsedTime !== undefined && _tmuteVars.adElapsedTime < _tmuteVars.adMinTime) return;

      mutePlayer();
    }
  }

  // (un)Mute Player
  function mutePlayer()
  {
    if (document.querySelectorAll(currentSelector.muteButton).length >= 1)
    {
      if (_tmuteVars.alreadyMuted === false) document.querySelectorAll(currentSelector.muteButton)[_tmuteVars.playerIdAds].click(); // If the player is already muted before an ad, we avoid to unmute it.
      _tmuteVars.playerMuted = !(_tmuteVars.playerMuted);

      if (_tmuteVars.playerMuted === true)
      {
        _tmuteVars.adsDisplayed++;
        _tmuteVars.adElapsedTime = 1;
        console.log("Ad #" + _tmuteVars.adsDisplayed + " detected. Player " + (_tmuteVars.alreadyMuted === true ? "already " : "") + "muted.");
        if (_tmuteVars.disableDisplay === true) document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].style.visibility = "hidden";
      } else {
        console.log("Ad #" + _tmuteVars.adsDisplayed + " finished (lasted " + _tmuteVars.adElapsedTime + "s)." + (_tmuteVars.alreadyMuted === true ? "" : " Player unmuted."));
        _tmuteVars.adElapsedTime = undefined;
        if (_tmuteVars.disableDisplay === true) document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].style.visibility = "visible";
      }
    } else {
      console.log("No volume button found (class changed ?).");
    }
  }
  
  // Manage ads options
  function adsOptions(changeType = "show")
  {
    switch(changeType) {
      // Manage player display during an ad (either hiding the ads or still showing them)
    	case "display":
        _tmuteVars.disableDisplay = !(_tmuteVars.disableDisplay);
        // Update the player display if an ad is supposedly in progress
        if (_tmuteVars.playerMuted === true) document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].style.visibility = (_tmuteVars.disableDisplay === true) ? "hidden" : "visible";
        document.getElementById("_tmads_display").innerText = (_tmuteVars.disableDisplay === true ? "Show" : "Hide") + " player during ads";
        break;
      // Force a player unlock if Twitch didn't remove the ad notice properly instead of waiting the auto unlock
    	case "unlock":
        var advert = document.getElementsByClassName(currentSelector.adNotice)[0];
        
        if (_tmuteVars.adElapsedTime === undefined && advert.childNodes[1] === undefined)
        {
          alert("There's no ad notice displayed. No unlock to do.");
        } else {
          // We set the elapsed time to the unlock timer to trigger it during the next check.
          _tmuteVars.adElapsedTime = _tmuteVars.adUnlockAt;
          console.log("Unlock requested.");
        }
        break;
      // Display the ads options button
      case "init":
        // Do the resets needed if we changed page during an ad
        if (_tmuteVars.playerMuted === true && window.location.pathname != _tmuteVars.currentPage) {
          mutePlayer();
        }
        _tmuteVars.currentPage = window.location.pathname;
        
        if (document.getElementsByClassName(currentSelector.viewersCount)[0] === undefined && document.getElementsByClassName(currentSelector.squadHeader)[0] === undefined) break;
        
        // Check ad notice class exists, otherwise we'll use the fallback class
        if (document.getElementsByClassName(currentSelector.adNotice)[0] === undefined)
        {
          _tmuteVars.classFallback = true;
          currentSelector.adNotice = currentSelector.adNoticeFallback;
          console.log("Main ad notice class not found, falling back on another one.");
          
          // If the fallback isn't found either, we try a last thing or we stop the script as Twitch did further changes that require a script update
          if (document.getElementsByClassName(currentSelector.adNotice)[0] === undefined)
          {
            clearInterval(_tmuteVars.autoCheck); // Temporarily stop the checks while we do a last search on a specific element that could still find the ad notice class
            console.log("Trying to retrieve the new ad notice class, 1st fallback one wasn't found, Twitch changed something. Feel free to contact the author of the script.");
            var lastFallback = document.querySelector("[data-a-target='ax-overlay']");
            
            // We found the new ad notice class, restarting the checks
            if (lastFallback !== null)
            {
              _tmuteVars.classFallback = true;
              currentSelector.adNotice = lastFallback.parentNode.className;
              console.log("New ad notice class retrieved (\"" + currentSelector.adNotice + "\") and set as new fallback.");
              _tmuteVars.autoCheck = setInterval(checkAd, _tmuteVars.timerCheck);
            } else {
            	console.log("Script stopped. Last fallback ad notice class not found either, Twitch changed something. Feel free to contact the author of the script.");
            }
          }
        }
        
        // Append ads options and events related
        var optionsTemplate = document.createElement("div");
        optionsTemplate.id = "_tmads_options-wrapper";
        optionsTemplate.className = "tw-inline-flex";
        optionsTemplate.style = "padding-top: 10px;";
        optionsTemplate.innerHTML = `
        <span id="_tmads_options" style="display: none;">
          <button type="button" id="_tmads_unlock" style="padding: 0 2px 0 2px; margin-left: 2px; height: 16px; width: unset;" class="tw-interactive tw-button-icon tw-button-icon--hollow">Unlock player</button>
          <button type="button" id="_tmads_display" style="padding: 0 2px 0 2px; margin-left: 2px; height: 16px; width: unset;" class="tw-interactive tw-button-icon tw-button-icon--hollow">` + (_tmuteVars.disableDisplay === true ? "Show" : "Hide") + ` player during ads</button>
        </span>
        <button type="button" id="_tmads_showoptions" style="padding: 0 2px 0 2px; margin-left: 2px; height: 16px; width: unset;" class="tw-interactive tw-button-icon tw-button-icon--hollow">Ads Options</button>`;
        
        // Normal player page
        if (document.getElementsByClassName(currentSelector.viewersCount)[0] !== undefined)
        {
          _tmuteVars.squadPage = false;
          _tmuteVars.playerIdAds = 0;
          document.getElementsByClassName(currentSelector.viewersCount)[0].parentNode.childNodes[1].appendChild(optionsTemplate);
        // Squad page
        } else if (document.getElementsByClassName(currentSelector.squadHeader)[0] !== undefined)
        {
          _tmuteVars.squadPage = true;
          _tmuteVars.playerIdAds = 0;
          // Since the primary player is never at the same place, we've to find it.
          for (var i = 0; i < parseInt(document.querySelectorAll(currentSelector.playerVideo).length); i++)
          {
            if (document.getElementsByClassName(currentSelector.squadPlayer)[0].childNodes[i].classList.contains(currentSelector.squadPlayerMain))
            {
              _tmuteVars.playerIdAds = i;
              break;
            }
          }
          document.getElementsByClassName(currentSelector.squadHeader)[0].appendChild(optionsTemplate);
        }
        
        document.getElementById("_tmads_showoptions").addEventListener("click", adsOptions, false);
        document.getElementById("_tmads_display").addEventListener("click", function() { adsOptions("display"); }, false);
        document.getElementById("_tmads_unlock").addEventListener("click", function() { adsOptions("unlock"); }, false);
        console.log("Ads options initialized.");
        
        break;
      // Display/Hide the ads options
    	case "show":
    	default:
        _tmuteVars.displayingOptions = !(_tmuteVars.displayingOptions);
        document.getElementById("_tmads_options").style.display = (_tmuteVars.displayingOptions === false) ? "none" : "inline-flex";
		} 
  }
  
  // Start the background check
  _tmuteVars.autoCheck = setInterval(checkAd, _tmuteVars.timerCheck);
  
})();