raingart / Nova YouTube

// ==UserScript==
// @name            Nova YouTube
// @namespace       https://github.com/raingart/Nova-YouTube-extension/
// @version         0.41.0
// @description     Gives you more control on YouTube

// @author          raingart <raingart+scriptaddons@protonmail.com>
// @license         Apache-2.0
// @icon            https://raw.github.com/raingart/Nova-YouTube-extension/master/icons/48.png

// @homepageURL     https://github.com/raingart/Nova-YouTube-extension
// @supportURL      https://github.com/raingart/Nova-YouTube-extension/issues
// @contributionURL https://www.patreon.com/raingart
// @contributionURL https://www.buymeacoffee.com/raingart
// @contributionURL https://www.paypal.com/donate/?hosted_button_id=B44WLWHZ8AGU2

// @domain          youtube.com
// @include         http*://www.youtube.com/*
// @include         http*://m.youtube.com/*
// @include         http*://*.youtube-nocookie.com/embed/*
// @include         http*://youtube.googleapis.com/embed/*
// @include         http*://raingart.github.io/options.html*

// @exclude         http*://*.youtube.com/*.xml*
// @exclude         http*://*.youtube.com/error*
// @exclude         http*://music.youtube.com/*
// @exclude         http*://accounts.youtube.com/*
// @exclude         http*://studio.youtube.com/*
// @exclude         http*://*.youtube.com/redirect?*
// @exclude         http*://*.youtube.com/embed/?*

// @grant           GM_getResourceText
// @grant           GM_getResourceURL
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           GM_registerMenuCommand
// @grant           GM_notification
// @grant           GM_openInTab

// @run-at          document-start

// @compatible      chrome >=80 Violentmonkey,Tampermonkey
// @compatible      firefox >=74 Tampermonkey
// ==/UserScript==
/*jshint esversion: 6 */

if (typeof GM_info === 'undefined') {
   alert('Direct Chromium is not supported now');
}
try {
   document?.body;
} catch (error) {
   errorAlert('Your browser does not support chaining operator');
}
switch (GM_info.scriptHandler) {
   case 'Tampermonkey':
   case 'Violentmonkey':
   case 'ScriptCat':
      break;
   case 'FireMonkey':
      errorAlert(GM_info.scriptHandler + ' incomplete support', true);
      break;
   case 'Greasemonkey':
      errorAlert(GM_info.scriptHandler + ' is not supported');
      break;
   default:
      if (typeof GM_getValue !== 'function') {
         errorAlert('Your ' + GM_info.scriptHandler + ' does not support/no access the API being used. Contact the developer')
      }
      break;
}
if (!('MutationObserver' in window)) {
   errorAlert('MutationObserver not supported');
}
function errorAlert(text = '', continue_execute) {
   alert(GM_info.script.name + ' Error!\n' + text);
   if (!continue_execute) {
      throw GM_info.script.name + ' crashed!\n' + text;
   }
}
window.nova_plugins = [];
window.nova_plugins.push({
   id: 'comments-visibility',
   run_on_pages: 'watch, -mobile',
   restart_on_location_change: true,
   _runtime: user_settings => {
      NOVA.collapseElement({
         selector: '#comments',
         remove: (user_settings.comments_visibility_mode == 'disable') ? true : false,
      });
   },
});
window.nova_plugins.push({
   id: 'square-avatars',
   run_on_pages: 'all, -live_chat',
   _runtime: user_settings => {
      NOVA.css.push(
         [
            'yt-img-shadow',
            '.ytp-title-channel-logo',
            '#player .ytp-title-channel',
            'ytm-profile-icon',
            'a.ytd-thumbnail',
         ]
            .join(',\n') + ` {
               border-radius: 0 !important;
            }`);
      NOVA.waitUntil(() => {
         if (window.yt && (obj = yt?.config_?.EXPERIMENT_FLAGS) && Object.keys(obj).length) {
            yt.config_.EXPERIMENT_FLAGS.web_rounded_thumbnails = false;
            return true;
         }
      });
   },
});
window.nova_plugins.push({
   id: 'comments-expand',
   run_on_pages: 'watch, -mobile',
   _runtime: user_settings => {
      NOVA.css.push(
         `#expander.ytd-comment-renderer {
            overflow-x: hidden;
         }`);
      NOVA.watchElements({
         selectors: ['#comment #expander[collapsed] #more:not([hidden])'],
         attr_mark: 'nova-comment-expanded',
         callback: btn => {
            const moreExpand = () => btn.click();
            const comment = btn.closest('#expander[collapsed]');
            switch (user_settings.comments_expand_mode) {
               case 'onhover':
                  comment.addEventListener('mouseenter', moreExpand, { capture: true, once: true });
                  break;
               case 'always':
                  moreExpand();
                  break;
            }
         },
      });
      NOVA.watchElements({
         selectors: ['#replies #more-replies button'],
         attr_mark: 'nova-replies-expanded',
         callback: btn => {
            const moreExpand = () => btn.click();
            switch (user_settings.comments_view_reply) {
               case 'onhover':
                  btn.addEventListener('mouseenter', moreExpand, { capture: true, once: true });
                  break;
               case 'always':
                  moreExpand();
                  break;
            }
         },
      });
      if (NOVA.queryURL.has('lc')) {
         NOVA.waitSelector('#comment #linked-comment-badge + #body #expander[collapsed] #more:not([hidden])')
            .then(btn => btn.click());
         NOVA.waitSelector('ytd-comment-thread-renderer:has(#linked-comment-badge) #replies #more-replies button')
            .then(btn => btn.click());
      }
   },
});
window.nova_plugins.push({
   id: 'channel-videos-count',
   run_on_pages: 'watch, -mobile',
   restart_on_location_change: true,
   opt_api_key_warn: true,
   _runtime: user_settings => {
      const
         CACHE_PREFIX = 'nova-channel-videos-count:',
         SELECTOR_ID = 'nova-video-count';
      switch (NOVA.currentPage) {
         case 'watch':
            NOVA.waitSelector('#upload-info #owner-sub-count, ytm-slim-owner-renderer .subhead')
               .then(el => setVideoCount(el));
            break;
      }
      function setVideoCount(container = required()) {
         const channelId = NOVA.getChannelId();
         if (!channelId) return console.error('setVideoCount channelId: empty', channelId);
         if (storage = sessionStorage.getItem(CACHE_PREFIX + channelId)) {
            insertToHTML({ 'text': storage, 'container': container });
         }
         else {
            NOVA.request.API({
               request: 'channels',
               params: { 'id': channelId, 'part': 'statistics' },
               api_key: user_settings['user-api-key'],
            })
               .then(res => {
                  if (res?.error) return alert(`Error [${res.code}]: ${res.reason}\n` + res.error);
                  res?.items?.forEach(item => {
                     if (videoCount = NOVA.prettyRoundInt(item.statistics.videoCount)) {
                        insertToHTML({ 'text': videoCount, 'container': container });
                        sessionStorage.setItem(CACHE_PREFIX + channelId, videoCount);
                     } else console.warn('API is change', item);
                  });
               });
         }
         function insertToHTML({ text = '', container = required() }) {
            if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
            (document.getElementById(SELECTOR_ID) || (function () {
               container.insertAdjacentHTML('beforeend',
                  `<span class="date style-scope ytd-video-secondary-info-renderer" style="margin-right:5px;"> • <span id="${SELECTOR_ID}">${text}</span> videos</span>`);
               return document.getElementById(SELECTOR_ID);
            })())
               .textContent = text;
            container.title = `${text} videos`;
         }
      }
   },
});
window.nova_plugins.push({
   id: 'save-to-playlist',
   run_on_pages: 'home, feed, channel, results, watch, -mobile',
   _runtime: user_settings => {
      NOVA.waitSelector('tp-yt-paper-dialog #playlists')
         .then(playlists => {
            const container = playlists.closest('tp-yt-paper-dialog');
            new IntersectionObserver(([entry]) => {
               const searchInput = container.querySelector('input[type=search]')
               if (entry.isIntersecting) {
                  if (user_settings.save_to_playlist_sort) sortPlaylistsMenu(playlists);
                  if (!searchInput) insertFilterInput(playlists);
               }
               else if (searchInput) {
                  searchInput.value = '';
                  searchInput.dispatchEvent(new Event('change'));
               }
            })
               .observe(container);
         });
      function sortPlaylistsMenu(playlists = required()) {
         if (!(playlists instanceof HTMLElement)) return console.error('playlists not HTMLElement:', playlists);
         playlists.append(
            ...Array.from(playlists.childNodes)
               .sort(sortByLabel)
         );
         function sortByLabel(a, b) {
            const getLabel = el => el.textContent.trim();
            return stringLocaleCompare(getLabel(a), getLabel(b));
            function stringLocaleCompare(a = required(), b = required()) {
               return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
            }
         }
      }
      function insertFilterInput(container = required()) {
         if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
         const searchInput = document.createElement('input');
         searchInput.setAttribute('type', 'search');
         searchInput.setAttribute('placeholder', 'Playlist Filter');
         Object.assign(searchInput.style, {
            padding: '.4em .6em',
            border: 0,
            outline: 0,
            width: '100%',
            'margin-bottom': '1.5em',
            height: '2.5em',
            color: 'var(--ytd-searchbox-text-color)',
            'background-color': 'var(--ytd-searchbox-background)',
         });
         ['change', 'keyup'].forEach(evt => {
            searchInput
               .addEventListener(evt, function () {
                  NOVA.searchFilterHTML({
                     'keyword': this.value,
                     'filter_selectors': '#playlists #checkbox',
                     'highlight_selector': '#label',
                  });
               });
            searchInput
               .addEventListener('click', () => {
                  searchInput.value = '';
                  searchInput.dispatchEvent(new Event('change'));
               });
         });
         container.prepend(searchInput);
      };
   },
});
window.nova_plugins.push({
   id: 'header-unfixed',
   run_on_pages: 'all, -embed, -mobile, -live_chat',
   _runtime: user_settings => {
      const
         CLASS_NAME_TOGGLE = 'nova-header-unfixed',
         SELECTOR = 'html.' + CLASS_NAME_TOGGLE;
      NOVA.css.push(
         `${SELECTOR} #masthead-container {
            position: absolute !important;
         }
         ${SELECTOR} #chips-wrapper {
            position: sticky !important;
         }
         ${SELECTOR} #header {
            margin-top: 0 !important;
         }`);
      document.documentElement.classList.add(CLASS_NAME_TOGGLE);
      if (user_settings.header_unfixed_hotkey) {
         const hotkey = user_settings.header_unfixed_hotkey || 'v';
         document.addEventListener('keyup', evt => {
            if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
            if (evt.key === hotkey) {
               document.documentElement.classList.toggle(CLASS_NAME_TOGGLE);
            }
         });
      }
      if (user_settings.header_unfixed_scroll) {
         createArrowButton();
         document.addEventListener('yt-action', evt => {
            if (evt.detail?.actionName == 'yt-store-grafted-ve-action'
            ) {
               scrollAfter();
            }
         });
         function scrollAfter() {
            if ((masthead = document.getElementById('masthead'))
               && (topOffset = masthead.offsetHeight)
               && NOVA.isInViewport(masthead)
            ) {
               window.scrollTo({ top: topOffset });
            }
         }
         function createArrowButton() {
            const scrollDownButton = document.createElement('button');
            scrollDownButton.textContent = '▼';
            scrollDownButton.title = 'Scroll down';
            Object.assign(scrollDownButton.style, {
               cursor: 'pointer',
               background: 'transparent',
               color: 'deepskyblue',
               border: 'none',
            });
            scrollDownButton.onclick = scrollAfter;
            if (endnode = document.getElementById('end')) {
               endnode.parentElement.insertBefore(scrollDownButton, endnode);
            }
         }
      }
   },
});
const NOVA = {
   waitSelector(selector = required(), container) {
      if (typeof selector !== 'string') return console.error('wait > selector:', typeof selector);
      if (container && !(container instanceof HTMLElement)) return console.error('wait > container not HTMLElement:', container);
      return new Promise(resolve => {
         if (element = (container || document.body || document).querySelector(selector)) {
            return resolve(element);
         }
         new MutationObserver((mutationRecordsArray, observer) => {
            for (const record of mutationRecordsArray) {
               for (const node of record.addedNodes) {
                  if (![1, 3, 8].includes(node.nodeType) || !(node instanceof HTMLElement)) continue;
                  if (node.matches && node.matches(selector)) {
                     observer.disconnect();
                     return resolve(node);
                  }
                  else if (
                     (parentEl = node.parentElement || node)
                     && (parentEl instanceof HTMLElement)
                     && (element = parentEl.querySelector(selector))
                  ) {
                     observer.disconnect();
                     return resolve(element);
                  }
               }
            }
            if (document?.readyState != 'loading'
               && (element = (container || document?.body || document).querySelector(selector))
            ) {
               observer.disconnect();
               return resolve(element);
            }
         })
            .observe(container || document.body || document.documentElement || document, {
               childList: true,
               subtree: true,
               attributes: true,
            });
      });
   },
   waitUntil(condition = required(), timeout = 100) {
      if (typeof condition !== 'function') return console.error('waitUntil > condition is not fn:', typeof condition);
      return new Promise((resolve) => {
         if (result = condition()) {
            resolve(result);
         }
         else {
            const interval = setInterval(() => {
               if (result = condition()) {
                  clearInterval(interval);
                  resolve(result);
               }
            }, timeout);
         }
      });
   },
   delay(ms = 100) {
      return new Promise(resolve => setTimeout(resolve, ms));
   },
   watchElements_list: {},
   watchElements({ selectors = required(), attr_mark, callback = required() }) {
      if (!Array.isArray(selectors) && typeof selectors !== 'string') return console.error('watch > selector:', typeof selectors);
      if (typeof callback !== 'function') return console.error('watch > callback:', typeof callback);
      this.waitSelector((typeof selectors === 'string') ? selectors : selectors.join(','))
         .then(video => {
            !Array.isArray(selectors) && (selectors = selectors.split(',').map(s => s.trim()));
            process();
            this.watchElements_list[attr_mark] = setInterval(() =>
               document.visibilityState == 'visible' && process(), 1000 * 1.5);
            function process() {
               selectors
                  .forEach(selectorItem => {
                     if (attr_mark) selectorItem += `:not([${attr_mark}])`;
                     document.body.querySelectorAll(selectorItem)
                        .forEach(el => {
                           if (attr_mark) el.setAttribute(attr_mark, true);
                           callback(el);
                        });
                  });
            }
         });
   },
   runOnPageInitOrTransition(callback) {
      if (!callback || typeof callback !== 'function') {
         return console.error('runOnPageInitOrTransition > callback not function:', ...arguments);
      }
      let prevURL = location.href;
      const isURLChange = () => (prevURL === location.href) ? false : prevURL = location.href;
      isURLChange() || callback();
      document.addEventListener('yt-navigate-finish', () => isURLChange() && callback());
   },
   css: {
      push(css = required(), selector, important) {
         if (typeof css === 'object') {
            if (!selector) return console.error('injectStyle > empty json-selector:', ...arguments);
            injectCss(selector + json2css(css));
            function json2css(obj) {
               let css = '';
               Object.entries(obj)
                  .forEach(([key, value]) => {
                     css += key + ':' + value + (important ? ' !important' : '') + ';';
                  });
               return `{ ${css} }`;
            }
         }
         else if (css && typeof css === 'string') {
            if (document.head) {
               injectCss(css);
            }
            else {
               window.addEventListener('load', () => injectCss(css), { capture: true, once: true });
            }
         }
         else {
            console.error('addStyle > css:', typeof css);
         }
         function injectCss(source = required()) {
            let sheet;
            if (source.endsWith('.css')) {
               sheet = document.createElement('link');
               sheet.rel = 'sheet';
               sheet.href = source;
            }
            else {
               const sheetId = 'NOVA-style';
               sheet = document.getElementById(sheetId) || (function () {
                  const style = document.createElement('style');
                  style.type = 'text/css';
                  style.id = sheetId;
                  return (document.head || document.documentElement).appendChild(style);
               })();
            }
            sheet.textContent += '\n' + source
               .replace(/\n+\s{2,}/g, ' ')
               + '\n';
         }
      },
      getValue(selector = required(), prop_name = required()) {
         return (el = (selector instanceof HTMLElement) ? selector : document.body?.querySelector(selector))
            ? getComputedStyle(el).getPropertyValue(prop_name) : null;
      },
   },
   prettyRoundInt(num) {
      num = +num;
      if (num === 0) return '';
      if (num < 1000) return num;
      const sizes = ['', 'K', 'M', 'B'];
      const i = Math.floor(Math.log(Math.abs(num)) / Math.log(1000));
      if (!sizes[i]) return num;
      return round(num / 1000 ** i, 1) + sizes[i];
      function round(n, precision = 2) {
         const prec = 10 ** precision;
         return Math.floor(n * prec) / prec;
      }
   },
   isInViewport(el = required()) {
      if (!(el instanceof HTMLElement)) return console.error('el is not HTMLElement type:', el);
      if (bounding = el.getBoundingClientRect()) {
         return (
            bounding.top >= 0 &&
            bounding.left >= 0 &&
            bounding.bottom <= window.innerHeight &&
            bounding.right <= window.innerWidth
         );
      }
   },
   collapseElement({ selector = required(), title = required(), remove }) {
      const selector_id = `${title.match(/[a-z]+/gi).join('')}-prevent-load-btn`;
      this.waitSelector(selector.toString())
         .then(el => {
            if (remove) el.remove();
            else {
               if (document.getElementById(selector_id)) return;
               el.style.display = 'none';
               const btn = document.createElement('a');
               btn.textContent = `Load ${title}`;
               btn.id = selector_id;
               btn.className = 'more-button style-scope ytd-video-secondary-info-renderer';
               Object.assign(btn.style, {
                  cursor: 'pointer',
                  'text-align': 'center',
                  'text-transform': 'uppercase',
                  display: 'block',
                  color: 'var(--yt-spec-text-secondary)',
               });
               btn.addEventListener('click', () => {
                  btn.remove();
                  el.style.display = 'unset';
                  window.dispatchEvent(new Event('scroll'));
               });
               el.before(btn);
            }
         });
   },
   aspectRatio: {
      sizeToFit({
         srcWidth = 0, srcHeight = 0,
         maxWidth = window.innerWidth, maxHeight = window.innerHeight
      }) {
         const aspectRatio = Math.min(+maxWidth / +srcWidth, +maxHeight / +srcHeight);
         return {
            width: +srcWidth * aspectRatio,
            height: +srcHeight * aspectRatio,
         };
      },
      getAspectRatio({ width = required(), height = required() }) {
         const
            gcd = (a, b) => b ? gcd(b, a % b) : a,
            divisor = gcd(width, height);
         return width / divisor + ':' + height / divisor;
      },
      chooseAspectRatio({ width = required(), height = required(), layout }) {
         const acceptedRatioList = {
            'landscape': {
               '1:1': 1,
               '3:2': 1.5,
               '4:3': 1.33333333333,
               '5:4': 1.25,
               '5:3': 1.66666666667,
               '16:9': 1.77777777778,
               '16:10': 1.6,
               '17:9': 1.88888888889,
               '21:9': 2.33333333333,
               '24:10': 2.4,
            },
            'portrait': {
               '1:1': 1,
               '2:3': .66666666667,
               '3:4': .75,
               '3:5': .6,
               '4:5': .8,
               '9:16': .5625,
               '9:17': .5294117647,
               '9:21': .4285714286,
               '10:16': .625,
            },
         };
         return choiceRatioFromList(this.getAspectRatio(...arguments)) || acceptedRatioList['landscape']['16:9'];
         function choiceRatioFromList(ratio = required()) {
            const layout_ = layout || ((ratio < 1) ? 'portrait' : 'landscape');
            return acceptedRatioList[layout_][ratio];
         }
      },
      calculateHeight: (width = required(), aspectRatio = (16 / 9)) => parseFloat((width / aspectRatio).toFixed(2)),
      calculateWidth: (height = required(), aspectRatio = (16 / 9)) => parseFloat((height * aspectRatio).toFixed(2)),
   },
   bezelTrigger(text) {
      if (!text) return;
      if (typeof this.fateBezel === 'number') clearTimeout(this.fateBezel);
      const bezelEl = document.body.querySelector('.ytp-bezel-text');
      if (!bezelEl) return console.warn(`bezelTrigger ${text}=>${bezelEl}`);
      const
         bezelContainer = bezelEl.parentElement.parentElement,
         BEZEL_SELECTOR_TOGGLE = '.ytp-text-root';
      if (!this.bezel_css_inited) {
         this.bezel_css_inited = true;
         this.css.push(
            `${BEZEL_SELECTOR_TOGGLE} { display: block !important; }
            ${BEZEL_SELECTOR_TOGGLE} .ytp-bezel-text-wrapper {
               pointer-events: none;
               z-index: 40 !important;
            }
            ${BEZEL_SELECTOR_TOGGLE} .ytp-bezel-text { display: inline-block !important; }
            ${BEZEL_SELECTOR_TOGGLE} .ytp-bezel { display: none !important; }`);
      }
      bezelEl.textContent = text;
      bezelContainer.classList.add(BEZEL_SELECTOR_TOGGLE);
      this.fateBezel = setTimeout(() => {
         bezelContainer.classList.remove(BEZEL_SELECTOR_TOGGLE);
         bezelEl.textContent = '';
      }, 600);
   },
   getChapterList(video_duration = required()) {
      if (NOVA.currentPage != 'embed'
         && (chapsCollect = getFromDescriptionText() || getFromDescriptionChaptersBlock())
         && chapsCollect.length
      ) {
         return chapsCollect;
      }
      else {
         chapsCollect = getFromAPI();
      }
      return chapsCollect;
      function getFromDescriptionText() {
         const selectorTimestampLink = 'a[href*="&t="]';
         let
            timestampsCollect = [],
            nowComment,
            prevSec = -1;
         [
            (
               document.body.querySelector('ytd-watch-flexy')?.playerData?.videoDetails.shortDescription
               || document.body.querySelector('ytd-watch-metadata #description.ytd-watch-metadata')?.textContent
            ),
            //...[...document.body.querySelectorAll(`#comments #comment #comment-content:has(${selectorTimestampLink})`)]
            ...[...document.body.querySelectorAll(`#comments #comment #comment-content ${selectorTimestampLink} + *:last-child`)]
               .map(el => ({
                  'source': 'comment',
                  'text': el.closest('#comment-content')?.textContent,
               })),
         ]
            .forEach(data => {
               if (timestampsCollect.length > 1) return;
               nowComment = Boolean(data?.source);
               (data?.text || data)
                  ?.split('\n')
                  .forEach(line => {
                     line = line?.toString().trim();
                     if (line.length > 5 && line.length < 200 && (timestamp = /((\d?\d:){1,2}\d{2})/g.exec(line))) {
                        timestamp = timestamp[0];
                        const
                           sec = NOVA.timeFormatTo.hmsToSec(timestamp),
                           timestampPos = line.indexOf(timestamp);
                        if (
                           (nowComment ? true : (sec > prevSec && sec < +video_duration))
                           && (timestampPos < 5 || (timestampPos + timestamp.length) === line.length)
                        ) {
                           if (nowComment) prevSec = sec;
                           timestampsCollect.push({
                              'sec': sec,
                              'time': timestamp,
                              'title': line
                                 .replace(timestamp, '')
                                 .trim().replace(/^[:\-–—|]|(\[\])?|[:\-–—.;|]$/g, '')
                                 //.trim().replace(/^([:\-–—|]|(\d+[\.)]))|(\[\])?|[:\-–—.;|]$/g, '')
                                 .trim()
                           });
                        }
                     }
                  });
            });
         if (timestampsCollect.length == 1 && timestampsCollect[0].sec < (video_duration / 4)) {
            return timestampsCollect;
         }
         else if (timestampsCollect.length > 1) {
            if (nowComment) {
               timestampsCollect = timestampsCollect.sort((a, b) => a.sec - b.sec);
            }
            return timestampsCollect;
         }
      }
      async function getFromDescriptionChaptersBlock() {
         await NOVA.delay(500);
         const selectorTimestampLink = 'a[href*="&t="]';
         let timestampsCollect = [];
         document.body.querySelectorAll(`#structured-description ${selectorTimestampLink}`)
            .forEach(chaperLink => {
               if (sec = parseInt(NOVA.queryURL.get('t', chaperLink.href))) {
                  timestampsCollect.push({
                     'time': NOVA.timeFormatTo.HMS.digit(sec),
                     'sec': sec,
                     'title': chaperLink.textContent.trim().split('\n')[0].trim(),
                  });
               }
            });
         if (timestampsCollect.length == 1 && timestampsCollect[0].sec < (video_duration / 4)) {
            return timestampsCollect;
         }
         else if (timestampsCollect.length > 1) {
            return timestampsCollect;
         }
      }
      function getFromAPI() {
         if (!window.ytPubsubPubsubInstance) {
            return console.warn('ytPubsubPubsubInstance is null:', ytPubsubPubsubInstance);
         }
         const data = Object.values((
            ytPubsubPubsubInstance.i
            || ytPubsubPubsubInstance.j
            || ytPubsubPubsubInstance.subscriptions_
         )
            .find(a => a?.player)
            .player.app
         )
            .find(a => a?.videoData)
            ?.videoData.multiMarkersPlayerBarRenderer;
         if (data?.markersMap?.length) {
            return data.markersMap[0].value.chapters
               ?.map(c => {
                  const sec = +c.chapterRenderer.timeRangeStartMillis / 1000;
                  return {
                     'sec': sec,
                     'time': NOVA.timeFormatTo.HMS.digit(sec),
                     'title':
                        c.chapterRenderer.title.simpleText
                        || c.chapterRenderer.title.runs[0].text,
                  };
               });
         }
      }
   },
   searchFilterHTML({ keyword = required(), filter_selectors = required(), highlight_selector }) {
      keyword = keyword.toString().toLowerCase();
      document.body.querySelectorAll(filter_selectors)
         .forEach(item => {
            const
               text = item.textContent,
               hasText = text?.toLowerCase().includes(keyword),
               highlight = el => {
                  if (el.innerHTML.includes('<mark ')) {
                     el.innerHTML = el.innerHTML
                        .replace(/<\/?mark[^>]*>/g, '');
                  }
                  item.style.display = hasText ? '' : 'none';
                  if (hasText && keyword) {
                     highlightTerm({
                        'target': el,
                        'keyword': keyword,
                     });
                  }
               };
            (highlight_selector ? item.querySelectorAll(highlight_selector) : [item])
               .forEach(highlight);
         });
      function highlightTerm({ target = required(), keyword = required(), highlightClass }) {
         const
            content = target.innerHTML,
            pattern = new RegExp('(>[^<.]*)?(' + keyword + ')([^<.]*)?', 'gi'),
            highlightStyle = highlightClass ? `class="${highlightClass}"` : 'style="background-color:#afafaf"',
            replaceWith = `$1<mark ${highlightStyle}>$2</mark>$3`,
            marked = content.replaceAll(pattern, replaceWith);
         return (target.innerHTML = marked) !== content;
      }
   },
   isMusic() {
      return checkMusicType();
      function checkMusicType() {
         const
            channelName = movie_player.getVideoData().author,
            titleStr = movie_player.getVideoData().title.toUpperCase(),
            titleWordsList = titleStr?.toUpperCase().match(/\w+/g),
            playerData = document.body.querySelector('ytd-watch-flexy')?.playerData;
         return [
            titleStr,
            location.href,
            channelName,
            playerData?.microformat?.playerMicroformatRenderer.category,
            playerData?.title,
         ]
            .some(i => i?.toUpperCase().includes('MUSIC'))
            || document.body.querySelector('#upload-info #channel-name .badge-style-type-verified-artist')
            || (channelName && /(VEVO|Topic|Records|AMV)$/.test(channelName))
            || (channelName && /(MUSIC|ROCK|SOUNDS|SONGS)/.test(channelName.toUpperCase()))
            || titleWordsList?.length && ['🎵', '♫', 'SONG', 'SOUND', 'SONGS', 'SOUNDTRACK', 'LYRIC', 'LYRICS', 'AMBIENT', 'MIX', 'VEVO', 'CLIP', 'KARAOKE', 'OPENING', 'COVER', 'COVERED', 'VOCAL', 'INSTRUMENTAL', 'ORCHESTRAL', 'DJ', 'DNB', 'BASS', 'BEAT', 'HITS', 'ALBUM', 'PLAYLIST', 'DUBSTEP', 'CHILL', 'RELAX', 'CLASSIC', 'CINEMATIC']
               .some(i => titleWordsList.includes(i))
            || ['OFFICIAL VIDEO', 'OFFICIAL AUDIO', 'FEAT.', 'FT.', 'LIVE RADIO', 'DANCE VER', 'HIP HOP', 'ROCK N ROLL', 'HOUR VER', 'HOURS VER', 'INTRO THEME']
               .some(i => titleStr.includes(i))
            || titleWordsList?.length && ['OP', 'ED', 'MV', 'OST', 'NCS', 'BGM', 'EDM', 'GMV', 'AMV', 'MMD', 'MAD']
               .some(i => titleWordsList.includes(i));
      }
   },
   timeFormatTo: {
      hmsToSec(str) {
         let
            parts = str?.split(':'),
            t = 0;
         switch (parts?.length) {
            case 2: t = (parts[0] * 60); break;
            case 3: t = (parts[0] * 60 * 60) + (parts[1] * 60); break;
            case 4: t = (parts[0] * 24 * 60 * 60) + (parts[1] * 60 * 60) + (parts[2] * 60); break;
         }
         return t + +parts.pop();
      },
      HMS: {
         digit(time_sec = required()) {
            const
               ts = Math.abs(+time_sec),
               d = ~~(ts / 86400),
               h = ~~((ts % 86400) / 3600),
               m = ~~((ts % 3600) / 60),
               s = Math.floor(ts % 60);
            return (d ? `${d}d ` : '')
               + (h ? (d ? h.toString().padStart(2, '0') : h) + ':' : '')
               + (h ? m.toString().padStart(2, '0') : m) + ':'
               + s.toString().padStart(2, '0');
         },
         abbr(time_sec = required()) {
            const
               ts = Math.abs(+time_sec),
               d = ~~(ts / 86400),
               h = ~~((ts % 86400) / 3600),
               m = ~~((ts % 3600) / 60),
               s = Math.floor(ts % 60);
            return (d ? `${d}d ` : '')
               + (h ? (d ? h.toString().padStart(2, '0') : h) + 'h' : '')
               + (m ? (h ? m.toString().padStart(2, '0') : m) + 'm' : '')
               + (s ? (m ? s.toString().padStart(2, '0') : s) + 's' : '');
         },
      },
      ago(date = required()) {
         if (!(date instanceof Date)) return console.error('"date" is not Date type:', date);
         const samples = [
            { label: 'year', seconds: 31536000 },
            { label: 'month', seconds: 2592000 },
            { label: 'day', seconds: 86400 },
            { label: 'hour', seconds: 3600 },
            { label: 'minute', seconds: 60 },
            { label: 'second', seconds: 1 }
         ];
         const
            now = date.getTime(),
            seconds = Math.floor((Date.now() - Math.abs(now)) / 1000),
            interval = samples.find(i => i.seconds < seconds),
            time = Math.floor(seconds / interval.seconds);
         return `${(now < 0 ? '-' : '') + time} ${interval.label}${time !== 1 ? 's' : ''}`;
      },
   },
   updateUrl: (new_url = required()) => window.history.replaceState(null, null, new_url),
   queryURL: {
      has: (query = required(), url_string) => new URL(url_string || location).searchParams.has(query.toString()),
      get: (query = required(), url_string) => new URL(url_string || location).searchParams.get(query.toString()),
      set(query_obj = {}, url_string) {
         if (typeof query_obj != 'object' || !Object.keys(query_obj).length) return console.error('query_obj:', query_obj)
         const url = new URL(url_string || location);
         Object.entries(query_obj).forEach(([key, value]) => url.searchParams.set(key, value));
         return url.toString();
      },
      remove(query = required(), url_string) {
         const url = new URL(url_string || location);
         url.searchParams.delete(query.toString());
         return url.toString();
      },
   },
   request: (() => {
      const API_STORE_NAME = 'YOUTUBE_API_KEYS';
      async function getKeys() {
         NOVA.log('request.API: fetch to youtube_api_keys.json');
         return await fetch('https://gist.githubusercontent.com/raingart/ff6711fafbc46e5646d4d251a79d1118/raw/youtube_api_keys.json')
            .then(res => res.text())
            .then(keys => {
               NOVA.log(`get and save keys in localStorage`, keys);
               localStorage.setItem(API_STORE_NAME, keys);
               return JSON.parse(keys);
            })
            .catch(error => {
               localStorage.removeItem(API_STORE_NAME);
               throw error;
            })
            .catch(reason => console.error('Error get keys:', reason));
      }
      return {
         async API({ request = required(), params = required(), api_key }) {
            const YOUTUBE_API_KEYS = localStorage.hasOwnProperty(API_STORE_NAME)
               ? JSON.parse(localStorage.getItem(API_STORE_NAME)) : await getKeys();
            if (!api_key && (!Array.isArray(YOUTUBE_API_KEYS) || !YOUTUBE_API_KEYS?.length)) {
               localStorage.hasOwnProperty(API_STORE_NAME) && localStorage.removeItem(API_STORE_NAME);
               return console.error('YOUTUBE_API_KEYS empty:', YOUTUBE_API_KEYS);
            }
            const referRandKey = arr => api_key || 'AIzaSy' + arr[Math.floor(Math.random() * arr.length)];
            const query = Object.keys(params)
               .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
               .join('&');
            const URL = `https://www.googleapis.com/youtube/v3/${request}?${query}&key=` + referRandKey(YOUTUBE_API_KEYS);
            return await fetch(URL)
               .then(response => response.json())
               .then(json => {
                  if (!json?.error && Object.keys(json).length) return json;
                  console.warn('used key:', NOVA.queryURL.get('key', URL));
                  if (json?.error && Object.keys(json.error).length) {
                     throw new Error(JSON.stringify(json?.error));
                  }
               })
               .catch(error => {
                  localStorage.removeItem(API_STORE_NAME);
                  console.error(`Request API failed:${URL}\n${error}`);
                  if (error?.message && (err = JSON.parse(error?.message))) {
                     return {
                        'code': err.code,
                        'reason': err.errors?.length && err.errors[0].reason,
                        'error': err.message,
                     };
                  }
               });
         },
      };
   })(),
   getPlayerState(state) {
      return {
         '-1': 'UNSTARTED',
         0: 'ENDED',
         1: 'PLAYING',
         2: 'PAUSED',
         3: 'BUFFERING',
         5: 'CUED'
      }[state || movie_player.getPlayerState()];
   },
   videoElement: (() => {
      const videoSelector = '#movie_player:not(.ad-showing) video';
      document.addEventListener('canplay', ({ target }) => {
         target.matches(videoSelector) && (NOVA.videoElement = target);
      }, { capture: true, once: true });
      document.addEventListener('play', ({ target }) => {
         target.matches(videoSelector) && (NOVA.videoElement = target);
      }, true);
   })(),
   isFullscreen: () => (
      movie_player.classList.contains('ytp-fullscreen')
      || (movie_player.hasOwnProperty('isFullscreen') && movie_player.isFullscreen())
   ),
   getChannelId(api_key) {
      const isChannelId = id => id && /UC([a-z0-9-_]{22})$/i.test(id);
      let result = [
         document.querySelector('meta[itemprop="channelId"][content]')?.content,
         (document.body.querySelector('ytd-app')?.__data?.data?.response
            || document.body.querySelector('ytd-app')?.data?.response
            || window.ytInitialData
         )
            ?.metadata?.channelMetadataRenderer?.externalId,
         document.querySelector('link[itemprop="url"][href]')?.href.split('/')[4],
         location.pathname.split('/')[2],
         document.body.querySelector('#video-owner a[href]')?.href.split('/')[4],
         document.body.querySelector('a.ytp-ce-channel-title[href]')?.href.split('/')[4],
         document.body.querySelector('ytd-watch-flexy')?.playerData?.videoDetails.channelId,
      ]
         .find(i => isChannelId(i));
      return result;
   },
   storage_obj_manager: {
      STORAGE_NAME: 'nova-channels-state',
      async initStorage() {
         this.channelId = location.search.includes('list=')
            ? (NOVA.queryURL.get('list') || movie_player?.getPlaylistId())
            : await NOVA.waitUntil(NOVA.getChannelId, 1000);
      },
      read(return_all) {
         if (store = JSON.parse(localStorage.getItem(this.STORAGE_NAME))) {
            return return_all ? store : store[this.channelId];
         }
      },
      write(obj_save) {
         if ((storage = this.read('all') || {})) {
            if (Object.keys(obj_save).length) {
               storage = Object.assign(storage, { [this.channelId]: obj_save });
            }
            else {
               delete storage[this.channelId];
            }
         }
         localStorage.setItem(this.STORAGE_NAME, JSON.stringify(storage));
      },
      _getParam(key = required()) {
         if (storage = this.read()) {
            return storage[key];
         }
      },
      async getParam(key = required()) {
         if (!this.channelId) await this.initStorage();
         return this._getParam(...arguments);
      },
      save(obj_save) {
         if (storage = this.read()) {
            obj_save = Object.assign(storage, obj_save);
         }
         this.write(obj_save);
      },
      remove(key) {
         if ((storage = this.read())) {
            delete storage[key];
            this.write(storage);
         }
      },
   },
   log() {
      if (this.DEBUG && arguments.length) {
         console.groupCollapsed(...arguments);
         console.trace();
         console.groupEnd();
      }
   }
}
window.nova_plugins.push({
   id: 'channel-default-tab',
   run_on_pages: 'channel',
   restart_on_location_change: true,
   _runtime: user_settings => {
      if (NOVA.channelTab) return;
      location.pathname += '/' + user_settings.channel_default_tab;
   },
});
window.nova_plugins.push({
   id: 'rss-link',
   run_on_pages: 'channel, playlist, -mobile',
   restart_on_location_change: true,
   _runtime: user_settings => {
      const
         SELECTOR_ID = 'nova-rss-link',
         rssLinkPrefix = '/feeds/videos.xml',
         playlistURL = rssLinkPrefix + '?playlist_id=' + NOVA.queryURL.get('list'),
         genChannelURL = channelId => rssLinkPrefix + '?channel_id=' + channelId;
      switch (NOVA.currentPage) {
         case 'channel':
            NOVA.waitSelector('#channel-header #links-holder #primary-links')
               .then(container => {
                  if (!parseInt(NOVA.css.getValue('#header div.banner-visible-area', 'height'))) {
                     container = document.body.querySelector('#channel-header #inner-header-container #buttons');
                  }
                  if (url = (document.querySelector('link[type="application/rss+xml"][href]')?.href
                     || genChannelURL(NOVA.getChannelId(user_settings['user-api-key'])))
                  ) {
                     insertToHTML({ 'url': url, 'container': container });
                  }
               });
            break;
         case 'playlist':
            NOVA.waitSelector('ytd-playlist-header-renderer .metadata-buttons-wrapper')
               .then(container => {
                  insertToHTML({ 'url': playlistURL, 'container': container, 'is_playlist': true });
               });
            break;
      }
      function insertToHTML({ url = required(), container = required(), is_playlist }) {
         if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
         (container.querySelector(`#${SELECTOR_ID}`) || (function () {
            const link = document.createElement('a');
            link.id = SELECTOR_ID;
            link.target = '_blank';
            link.className = `yt-spec-button-shape-next--overlay`;
            link.innerHTML =
               `<svg viewBox="-35 -35 55 55" height="100%" width="100%" style="width: auto;">
                  <g fill="currentColor">
                     <path fill="#F60" d="M-17.392 7.875c0 3.025-2.46 5.485-5.486 5.485s-5.486-2.46-5.486-5.485c0-3.026 2.46-5.486 5.486-5.486s5.486 2.461 5.486 5.486zm31.351 5.486C14.042.744 8.208-11.757-1.567-19.736c-7.447-6.217-17.089-9.741-26.797-9.708v9.792C-16.877-19.785-5.556-13.535.344-3.66a32.782 32.782 0 0 1 4.788 17.004h8.827v.017zm-14.96 0C-.952 5.249-4.808-2.73-11.108-7.817c-4.821-3.956-11.021-6.184-17.255-6.15v8.245c6.782-.083 13.432 3.807 16.673 9.774a19.296 19.296 0 0 1 2.411 9.326h8.278v-.017z"/>
                  </g>
               </svg>`;
            Object.assign(link.style, {
               height: '20px',
               display: 'inline-block',
               padding: '5px',
            });
            if (is_playlist) {
               Object.assign(link.style, {
                  'margin-right': '8px',
                  'border-radius': '20px',
                  'background-color': 'var(--yt-spec-static-overlay-button-secondary)',
                  color: 'var(--yt-spec-static-overlay-text-primary)',
                  padding: '8px',
                  'margin-right': '8px',
                  'white-space': 'nowrap',
                  'font-size': 'var(--ytd-tab-system-font-size, 1.4rem)',
                  'font-weight': 'var(--ytd-tab-system-font-weight, 500)',
                  'letter-spacing': 'var(--ytd-tab-system-letter-spacing, .007px)',
                  'text-transform': 'var(--ytd-tab-system-text-transform, uppercase)',
               });
            }
            container.prepend(link);
            return link;
         })())
            .href = url;
      }
   },
});
window.nova_plugins.push({
   id: 'thumbs-hide',
   run_on_pages: 'home, results, feed, channel, watch, -mobile',
   _runtime: user_settings => {
      const
         thumbsSelectors = [
            'ytd-rich-item-renderer',
            'ytd-video-renderer',
            'ytd-grid-video-renderer',
            'ytd-compact-video-renderer',
            'ytm-compact-video-renderer',
            'ytm-item-section-renderer'
         ]
            .join(',');
      document.addEventListener('yt-action', evt => {
         if ([
            'yt-append-continuation-items-action',
            'ytd-update-grid-state-action',
            'yt-service-request',
            'ytd-rich-item-index-update-action',
         ]
            .includes(evt.detail?.actionName)
         ) {
            switch (NOVA.currentPage) {
               case 'home':
                  thumbRemove.live();
                  thumbRemove.mix();
                  thumbRemove.watched();
                  break;
               case 'results':
                  thumbRemove.live();
                  thumbRemove.shorts();
                  thumbRemove.mix();
                  break;
               case 'feed':
                  thumbRemove.live();
                  thumbRemove.streamed();
                  thumbRemove.shorts();
                  thumbRemove.durationLimits();
                  thumbRemove.premieres();
                  thumbRemove.mix();
                  thumbRemove.watched();
                  break;
               case 'channel':
                  thumbRemove.live();
                  thumbRemove.streamed();
                  thumbRemove.premieres();
                  thumbRemove.watched();
                  break;
               case 'watch':
                  thumbRemove.live();
                  thumbRemove.mix();
                  thumbRemove.watched();
                  break;
            }
         }
      });
      const thumbRemove = {
         shorts() {
            if (!user_settings.shorts_disable) return;
            if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'shorts') return;
            document.body.querySelectorAll('a#thumbnail[href*="shorts/"]')
               .forEach(el => el.closest(thumbsSelectors)?.remove());
         },
         durationLimits() {
            if (!+user_settings.shorts_disable_min_duration) return;
            const OVERLAYS_TIME_SELECTOR = '#thumbnail #overlays #text:not(:empty)';
            NOVA.waitSelector(OVERLAYS_TIME_SELECTOR)
               .then(() => {
                  document.body.querySelectorAll(OVERLAYS_TIME_SELECTOR)
                     .forEach(el => {
                        if ((thumb = el.closest(thumbsSelectors))
                           && (time = NOVA.timeFormatTo.hmsToSec(el.textContent.trim()))
                           && time < (+user_settings.shorts_disable_min_duration || 60)
                        ) {
                           thumb.remove();
                        }
                     });
               });
         },
         premieres() {
            if (!user_settings.premieres_disable) return;
            document.body.querySelectorAll(
               `#thumbnail #overlays [aria-label="Premiere"],
               #thumbnail #overlays [aria-label="Upcoming"]`
            )
               .forEach(el => el.closest(thumbsSelectors)?.remove());
            document.body.querySelectorAll('#video-badges > [class*="live-now"]')
               .forEach(el => el.closest(thumbsSelectors)?.remove());
         },
         live() {
            if (!user_settings.live_disable) return;
            if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'streams') return;
            document.body.querySelectorAll('#thumbnail img[src*="_live.jpg"]')
               .forEach(el => el.closest(thumbsSelectors)?.remove());
         },
         streamed() {
            if (!user_settings.streamed_disable) return;
            if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'streams') return;
            document.body.querySelectorAll('#metadata-line > span:last-of-type')
               .forEach(el => {
                  if (el.textContent?.split(' ').length === 4
                     && (thumb = el.closest(thumbsSelectors))) {
                     thumb.remove();
                  }
               });
         },
         mix() {
            if (!user_settings.mix_disable) return;
            document.body.querySelectorAll(
               `a[href*="list="][href*="start_radio="]:not([hidden]),
               #video-title[title^="Mix -"]:not([hidden])`
            )
               .forEach(el => el.closest('ytd-radio-renderer, ytd-compact-radio-renderer, ' + thumbsSelectors)?.remove());
         },
         watched() {
            if (!user_settings.watched_disable) return;
            if (!user_settings['thumbnails-watched']) return;
            const PERCENT_COMPLETE = user_settings.watched_disable_percent_complete || 90;
            document.body.querySelectorAll('#thumbnail #overlays #progress')
               .forEach(el => {
                  if (parseInt(el.style.width) > PERCENT_COMPLETE) {
                     el.closest(thumbsSelectors)?.remove();
                  }
               });
         },
      };
      if (user_settings.mix_disable) {
         NOVA.css.push(
            `ytd-radio-renderer {
               display: none !important;
            }`);
      }
   },
});
window.nova_plugins.push({
   id: 'shorts-redirect',
   run_on_pages: 'shorts',
   restart_on_location_change: true,
   _runtime: user_settings => {
      location.href = location.href.replace('shorts/', 'watch?v=');
   },
});
window.nova_plugins.push({
   id: 'pause-background-tab',
   run_on_pages: 'watch, embed',
   _runtime: user_settings => {
      if (location.hostname.includes('youtube-nocookie.com')) location.hostname = 'youtube.com';
      if (typeof window === 'undefined') return;
      const
         storeName = 'nova-playing-instanceIDTab',
         instanceID = String(Math.random()),
         removeStorage = () => localStorage.removeItem(storeName);
      NOVA.waitSelector('video')
         .then(video => {
            video.addEventListener('play', checkInstance);
            video.addEventListener('playing', checkInstance);
            ['pause', 'suspend', 'ended'].forEach(evt => video.addEventListener(evt, removeStorage));
            window.addEventListener('beforeunload', removeStorage);
            window.addEventListener('storage', store => {
               if ((!document.hasFocus() || NOVA.currentPage == 'embed')
                  && store.key === storeName && store.storageArea === localStorage
                  && localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID
                  && 'PLAYING' == NOVA.getPlayerState()
               ) {
                  video.pause();
               }
            });
            if (user_settings.pause_background_tab_autoplay_onfocus) {
               window.addEventListener('focus', () => {
                  if (!localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID
                     && ['UNSTARTED', 'PAUSED'].includes(NOVA.getPlayerState())
                  ) {
                     video.play();
                  }
               });
            }
            if (user_settings.pause_background_tab_autopause_unfocus) {
               window.addEventListener('blur', () => {
                  if (!document.hasFocus() && 'PLAYING' == NOVA.getPlayerState()) {
                     video.pause();
                  }
               });
            }
            function checkInstance() {
               if (localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID) {
                  video.pause();
               }
               else {
                  localStorage.setItem(storeName, instanceID);
               }
            }
         });
   },
});
window.nova_plugins.push({
   id: 'rate-wheel',
   run_on_pages: 'watch, embed',
   _runtime: user_settings => {
      NOVA.waitSelector('#movie_player video')
         .then(video => {
            const sliderContainer = insertSlider.apply(video);
            video.addEventListener('ratechange', function () {
               NOVA.bezelTrigger(this.playbackRate + 'x');
               if (Object.keys(sliderContainer).length) {
                  sliderContainer.slider.value = this.playbackRate;
                  sliderContainer.sliderLabel.textContent = `Speed (${this.playbackRate})`;
                  sliderContainer.sliderCheckbox.checked = (this.playbackRate === 1) ? false : true;
               }
            });
            setDefaultRate.apply(video);
            video.addEventListener('loadeddata', setDefaultRate);
            if (Object.keys(sliderContainer).length) {
               sliderContainer.slider.addEventListener('input', ({ target }) => playerRate.set(target.value));
               sliderContainer.slider.addEventListener('change', ({ target }) => playerRate.set(target.value));
               sliderContainer.slider.addEventListener('wheel', evt => {
                  evt.preventDefault();
                  const rate = playerRate.adjust(+user_settings.rate_step * Math.sign(evt.wheelDelta));
               });
               sliderContainer.sliderCheckbox.addEventListener('change', ({ target }) => {
                  target.checked || playerRate.set(1)
               });
            }
            NOVA.runOnPageInitOrTransition(async () => {
               if (NOVA.currentPage == 'watch') {
                  if (user_settings['save-channel-state']) {
                     if (userRate = await NOVA.storage_obj_manager.getParam('speed')) {
                        video.addEventListener('canplay', () => playerRate.set(userRate), { capture: true, once: true });
                     }
                  }
                  expandAvailableRatesMenu();
               }
            });
         });
      if (user_settings.rate_hotkey) {
         NOVA.waitSelector('.html5-video-container')
            .then(container => {
               container.addEventListener('wheel', evt => {
                  evt.preventDefault();
                  if (evt[user_settings.rate_hotkey]
                     || (user_settings.rate_hotkey == 'none' && !evt.ctrlKey && !evt.altKey && !evt.shiftKey && !evt.metaKey)) {
                     const rate = playerRate.adjust(+user_settings.rate_step * Math.sign(evt.wheelDelta));
                  }
               });
            });
      }
      if (+user_settings.rate_default !== 1 && user_settings.rate_default_apply_music) {
         NOVA.waitSelector('#upload-info #channel-name .badge-style-type-verified-artist')
            .then(icon => playerRate.set(1));
         NOVA.waitSelector('#upload-info #channel-name a[href]')
            .then(channelName => {
               if (/(VEVO|Topic|Records|AMV)$/.test(channelName.textContent)
                  || channelName.textContent.toUpperCase().includes('MUSIC')
               ) {
                  playerRate.set(1);
               }
            });
      }
      const playerRate = {
         testDefault: rate => (+rate % .25) === 0
            && +rate <= 2
            && +user_settings.rate_default <= 2
            && (typeof movie_player !== 'undefined' && movie_player.hasOwnProperty('getPlaybackRate')),
         async set(level = 1) {
            this.log('set', ...arguments);
            if (this.testDefault(level)) {
               this.log('set:default');
               movie_player.setPlaybackRate(+level) && this.saveInSession(level);
            }
            else {
               this.log('set:html5');
               if (NOVA.videoElement) {
                  NOVA.videoElement.playbackRate = +level;
                  this.clearInSession();
               }
            }
         },
         adjust(rate_step = required()) {
            this.log('adjust', ...arguments);
            return this.testDefault(rate_step) ? this.default(+rate_step) : this.html5(+rate_step);
         },
         default(playback_rate = required()) {
            this.log('default', ...arguments);
            const playbackRate = movie_player.getPlaybackRate();
            const inRange = step => {
               const setRateStep = playbackRate + step;
               return (.1 <= setRateStep && setRateStep <= 2) && +setRateStep.toFixed(2);
            };
            const newRate = inRange(+playback_rate);
            if (newRate && newRate != playbackRate) {
               movie_player.setPlaybackRate(newRate);
               if (newRate === movie_player.getPlaybackRate()) {
                  this.saveInSession(newRate);
               }
               else {
                  console.error('playerRate:default different: %s!=%s', newRate, movie_player.getPlaybackRate());
               }
            }
            this.log('default return', newRate);
            return newRate === movie_player.getPlaybackRate() && newRate;
         },
         html5(playback_rate = required()) {
            this.log('html5', ...arguments);
            if (!NOVA.videoElement) return console.error('playerRate > videoElement empty:', NOVA.videoElement);
            const playbackRate = NOVA.videoElement.playbackRate;
            const inRange = step => {
               const setRateStep = playbackRate + step;
               return (.1 <= setRateStep && setRateStep <= 3) && +setRateStep.toFixed(2);
            };
            const newRate = inRange(+playback_rate);
            if (newRate && newRate != playbackRate) {
               NOVA.videoElement.playbackRate = newRate;
               if (newRate === NOVA.videoElement.playbackRate) {
                  this.clearInSession();
               }
               else {
                  console.error('playerRate:html5 different: %s!=%s', newRate, NOVA.videoElement.playbackRate);
               }
            }
            this.log('html5 return', newRate);
            return newRate === NOVA.videoElement.playbackRate && newRate;
         },
         saveInSession(level = required()) {
            try {
               sessionStorage['yt-player-playback-rate'] = JSON.stringify({
                  creation: Date.now(), data: level.toString(),
               })
               this.log('playbackRate save in session:', ...arguments);
            } catch (err) {
               console.warn(`${err.name}: save "rate" in sessionStorage failed. It seems that "Block third-party cookies" is enabled`, err.message);
            }
         },
         clearInSession() {
            const keyName = 'yt-player-playback-rate';
            try {
               sessionStorage.hasOwnProperty(keyName) && sessionStorage.removeItem(keyName);
               this.log('playbackRate save in session:', ...arguments);
            } catch (err) {
               console.warn(`${err.name}: save "rate" in sessionStorage failed. It seems that "Block third-party cookies" is enabled`, err.message);
            }
         },
         log() {
            if (this.DEBUG && arguments.length) {
               console.groupCollapsed(...arguments);
               console.trace();
               console.groupEnd();
            }
         },
      };
      function setDefaultRate() {
         if (+user_settings.rate_default !== 1) {
            const is_music = NOVA.isMusic();
            if (this.playbackRate !== +user_settings.rate_default
               && (!user_settings.rate_default_apply_music || !is_music)
               && (!isNaN(this.duration) && this.duration > 25)
            ) {
               playerRate.set(user_settings.rate_default);
            }
            else if (this.playbackRate !== 1 && is_music) {
               playerRate.set(1);
            }
         }
      }
      function insertSlider() {
         const
            SELECTOR_ID = 'nova-rate-slider-menu',
            SELECTOR = '#' + SELECTOR_ID;
         NOVA.css.push(
            `${SELECTOR} [type="range"] {
               vertical-align: text-bottom;
               margin: '0 5px',
            }
            ${SELECTOR} [type="checkbox"] {
               appearance: none;
               outline: none;
               cursor: pointer;
            }
            ${SELECTOR} [type="checkbox"]:checked {
               background: #f00;
            }
            ${SELECTOR} [type="checkbox"]:checked:after {
               left: 20px;
               background-color: #fff;
            }`);
         const slider = document.createElement('input');
         slider.className = 'ytp-menuitem-slider';
         slider.type = 'range';
         slider.min = +user_settings.rate_step;
         slider.max = Math.max(2, +user_settings.rate_default);
         slider.step = +user_settings.rate_step;
         slider.value = this.playbackRate;
         const sliderIcon = document.createElement('div');
         sliderIcon.className = 'ytp-menuitem-icon';
         const sliderLabel = document.createElement('div');
         sliderLabel.className = 'ytp-menuitem-label';
         sliderLabel.textContent = `Speed (${this.playbackRate})`;
         const sliderCheckbox = document.createElement('input');
         sliderCheckbox.className = 'ytp-menuitem-toggle-checkbox';
         sliderCheckbox.type = 'checkbox';
         sliderCheckbox.title = 'Remember speed';
         const out = {};
         const right = document.createElement('div');
         right.className = 'ytp-menuitem-content';
         out.sliderCheckbox = right.appendChild(sliderCheckbox);
         out.slider = right.appendChild(slider);
         const speedMenu = document.createElement('div');
         speedMenu.className = 'ytp-menuitem';
         speedMenu.id = SELECTOR_ID;
         speedMenu.append(sliderIcon);
         out.sliderLabel = speedMenu.appendChild(sliderLabel);
         speedMenu.append(right);
         document.body.querySelector('.ytp-panel-menu')
            ?.append(speedMenu);
         return out;
      }
      function expandAvailableRatesMenu() {
         if (typeof _yt_player !== 'object') {
            return console.error('expandAvailableRatesMenu > _yt_player is empty', _yt_player);
         }
         if (path = findPathInObj({ 'obj': _yt_player, 'keys': 'getAvailablePlaybackRates' })) {
            setAvailableRates(_yt_player, 0, path.split('.'));
         }
         function findPathInObj({ obj = required(), keys = required(), path }) {
            const setPath = d => (path ? path + '.' : '') + d;
            for (const prop in obj) {
               if (obj.hasOwnProperty(prop) && obj[prop]) {
                  if (keys === prop) {
                     return this.path = setPath(prop)
                  }
                  if (obj[prop].constructor.name == 'Function' && Object.keys(obj[prop]).length) {
                     for (const j in obj[prop]) {
                        if (typeof obj[prop] !== 'undefined') {
                           findPathInObj({
                              'obj': obj[prop][j],
                              'keys': keys,
                              'path': setPath(prop) + '.' + j,
                           });
                        }
                        if (this.path) return this.path;
                     }
                  }
               }
            }
         }
         function setAvailableRates(path, idx, arr) {
            if (arr.length - 1 == idx) {
               path[arr[idx]] = () => [.25, .5, .75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4, 10];
            }
            else {
               setAvailableRates(path[arr[idx]], idx + 1, arr);
            }
         }
      }
   },
});
window.nova_plugins.push({
   id: 'volume-wheel',
   run_on_pages: 'watch, embed, -mobile',
   _runtime: user_settings => {
      NOVA.waitSelector('video')
         .then(video => {
            video.addEventListener('volumechange', function () {
               NOVA.bezelTrigger(movie_player.getVolume() + '%');
               playerVolume.buildVolumeSlider();
               if (user_settings.volume_mute_unsave) {
                  playerVolume.saveInSession(movie_player.getVolume());
               }
            });
            if (user_settings.volume_hotkey) {
               document.body.querySelector('.html5-video-container')
                  .addEventListener('wheel', evt => {
                     evt.preventDefault();
                     if (evt[user_settings.volume_hotkey] || (user_settings.volume_hotkey == 'none' && !evt.ctrlKey && !evt.altKey && !evt.shiftKey && !evt.metaKey)) {
                        if (step = +user_settings.volume_step * Math.sign(evt.wheelDelta)) {
                           playerVolume.adjust(step);
                        }
                     }
                  });
            }
            if (+user_settings.volume_level_default) {
               playerVolume.set(+user_settings.volume_level_default);
            }
            if (user_settings['save-channel-state']) {
               NOVA.runOnPageInitOrTransition(async () => {
                  if (NOVA.currentPage == 'watch' && (userVolume = await NOVA.storage_obj_manager.getParam('volume'))) {
                     video.addEventListener('canplay', () => playerVolume.set(userVolume), { capture: true, once: true });
                  }
               });
            }
         });
      const playerVolume = {
         adjust(delta) {
            const level = movie_player?.getVolume() + +delta;
            return user_settings.volume_unlimit ? this.unlimit(level) : this.set(level);
         },
         set(level = 50) {
            if (typeof movie_player === 'undefined' || !movie_player.hasOwnProperty('getVolume')) return console.error('Error getVolume');
            const newLevel = Math.max(0, Math.min(100, +level));
            if (newLevel !== movie_player.getVolume()) {
               movie_player.isMuted() && movie_player.unMute();
               movie_player.setVolume(newLevel);
               if (newLevel === movie_player.getVolume()) {
                  this.saveInSession(newLevel);
               }
               else {
                  console.error('setVolumeLevel error! Different: %s!=%s', newLevel, movie_player.getVolume());
               }
            }
            return newLevel === movie_player.getVolume() && newLevel;
         },
         saveInSession(level = required()) {
            const storageData = {
               creation: Date.now(),
               data: { 'volume': +level, 'muted': (level ? 'false' : 'true') },
            };
            try {
               localStorage['yt-player-volume'] = JSON.stringify(
                  Object.assign({ expiration: Date.now() + 2592e6 }, storageData)
               );
               sessionStorage['yt-player-volume'] = JSON.stringify(storageData);
            } catch (err) {
               console.warn(`${err.name}: save "volume" in sessionStorage failed. It seems that "Block third-party cookies" is enabled`, err.message);
            }
         },
         unlimit(level = 300) {
            if (level > 100) {
               if (!this.audioCtx) {
                  this.audioCtx = new AudioContext();
                  const source = this.audioCtx.createMediaElementSource(NOVA.videoElement);
                  this.node = this.audioCtx.createGain();
                  this.node.gain.value = 1;
                  source.connect(this.node);
                  this.node.connect(this.audioCtx.destination);
               }
               if (this.node.gain.value < 7) this.node.gain.value += 1;
               NOVA.bezelTrigger(movie_player.getVolume() * this.node.gain.value + '%');
            }
            else {
               if (this.audioCtx && this.node.gain.value !== 1) {
                  this.node.gain.value = 1;
               }
               this.set(level);
            }
         },
         buildVolumeSlider(timeout_ms = 800) {
            if (volumeArea = movie_player?.querySelector('.ytp-volume-area')) {
               if (typeof this.showTimeout === 'number') clearTimeout(this.showTimeout);
               volumeArea.dispatchEvent(new Event('mouseover', { bubbles: true }));
               this.showTimeout = setTimeout(() =>
                  volumeArea.dispatchEvent(new Event('mouseout', { bubbles: true }))
                  , timeout_ms);
               insertToHTML({
                  'text': Math.round(movie_player.getVolume()),
                  'container': volumeArea,
               });
            }
            function insertToHTML({ text = '', container = required() }) {
               if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
               const SELECTOR_ID = 'nova-volume-text';
               (document.getElementById(SELECTOR_ID) || (function () {
                  const SELECTOR = '#' + SELECTOR_ID;
                  NOVA.css.push(`
                     ${SELECTOR} {
                        display: none;
                        text-indent: 2px;
                        font-size: 110%;
                        text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
                        cursor: default;
                     }
                     ${SELECTOR}:after { content: '%'; }
                     .ytp-volume-control-hover:not([aria-valuenow="0"])+${SELECTOR} {
                        display: block;
                     }`)
                  const el = document.createElement('span');
                  el.id = SELECTOR_ID;
                  container.insertAdjacentElement('beforeend', el);
                  return el;
               })())
                  .textContent = text;
               container.title = `${text} %`;
            }
         }
      };
   },
});
window.nova_plugins.push({
   id: 'video-autopause',
   run_on_pages: 'watch, embed',
   restart_on_location_change: true,
   _runtime: user_settings => {
      if (user_settings['video-stop-preload'] && !user_settings.stop_preload_embed) return;
      if (user_settings.video_autopause_embed && NOVA.currentPage != 'embed') return;
      if (NOVA.currentPage == 'embed'
         && window.self !== window.top
         && ['0', 'false'].includes(NOVA.queryURL.get('autoplay'))
      ) {
         return;
      }
      NOVA.waitSelector('#movie_player video')
         .then(video => {
            if (user_settings.video_autopause_ignore_live && movie_player.getVideoData().isLive) return;
            forceVideoPause.apply(video);
         });
      function forceVideoPause() {
         if (user_settings.video_autopause_ignore_playlist && location.search.includes('list=')) return;
         this.pause();
         const forceHoldPause = setInterval(() => this.paused || this.pause(), 200);
         document.addEventListener('click', stopForceHoldPause);
         document.addEventListener('keyup', keyupSpace);
         function stopForceHoldPause() {
            if (movie_player.contains(document.activeElement)) {
               clearInterval(forceHoldPause);
               document.removeEventListener('keyup', keyupSpace);
               document.removeEventListener('click', stopForceHoldPause);
            }
         }
         function keyupSpace(evt) {
            switch (evt.code) {
               case 'Space':
                  stopForceHoldPause()
                  break;
            }
         }
      }
   },
});
window.nova_plugins.push({
   id: 'player-pin-scroll',
   run_on_pages: 'watch, -mobile',
   _runtime: user_settings => {
      if (!('IntersectionObserver' in window)) return alert('Nova\n\nPin player Error!\nIntersectionObserver not supported.');
      const
         CLASS_VALUE = 'nova-player-pin',
         PINNED_SELECTOR = '.' + CLASS_VALUE,
         UNPIN_BTN_CLASS_VALUE = CLASS_VALUE + '-unpin-btn',
         UNPIN_BTN_SELECTOR = '.' + UNPIN_BTN_CLASS_VALUE;
      document.addEventListener('scroll', () => {
         NOVA.waitSelector('#ytd-player')
            .then(container => {
               new IntersectionObserver(([entry]) => {
                  if (entry.isIntersecting) {
                     movie_player.classList.remove(CLASS_VALUE);
                     drag.reset();
                  }
                  else if (!movie_player.isFullscreen()
                     && document.documentElement.scrollTop
                  ) {
                     movie_player.classList.add(CLASS_VALUE);
                     drag?.storePos?.X && drag.setTranslate(drag.storePos);
                  }
                  window.dispatchEvent(new Event('resize'));
               }, {
                  threshold: .5,
               })
                  .observe(container);
            });
      }, { capture: true, once: true });
      NOVA.waitSelector(PINNED_SELECTOR)
         .then(async player => {
            drag.init(player);
            await NOVA.waitUntil(
               () => (NOVA.videoElement?.videoWidth && !isNaN(NOVA.videoElement.videoWidth)
                  && NOVA.videoElement?.videoHeight && !isNaN(NOVA.videoElement.videoHeight)
               )
               , 500)
            initMiniStyles();
            insertUnpinButton(player);
            document.addEventListener('fullscreenchange', () => NOVA.isFullscreen() && movie_player.classList.remove(CLASS_VALUE));
            NOVA.waitSelector('#movie_player video')
               .then(video => {
                  video.addEventListener('loadeddata', () => {
                     if (NOVA.currentPage != 'watch') return;
                     NOVA.waitSelector(PINNED_SELECTOR)
                        .then(() => {
                           const width = NOVA.aspectRatio.calculateWidth(
                              movie_player.clientHeight,
                              NOVA.aspectRatio.chooseAspectRatio({
                                 'width': NOVA.videoElement.videoWidth,
                                 'height': NOVA.videoElement.videoHeight,
                                 'layout': 'landscape',
                              }),
                           );
                           player.style.setProperty('--width', `${width}px !important;`);
                        });
                  });
               });
            if (user_settings.player_float_scroll_after_fullscreen_restore_srcoll_pos) {
               let scrollPos = 0;
               document.addEventListener('fullscreenchange', () => {
                  if (!NOVA.isFullscreen()
                     && scrollPos
                     && drag.storePos
                  ) {
                     window.scrollTo({
                        top: scrollPos,
                     });
                  }
               });
               document.addEventListener('yt-action', function (evt) {
                  if (evt.detail?.actionName == 'yt-close-all-popups-action') {
                     scrollPos = document.documentElement.scrollTop;
                  }
               });
               document.addEventListener('yt-navigate-start', () => scrollPos = 0);
            }
         });
      function initMiniStyles() {
         const scrollbarWidth = (window.innerWidth - document.documentElement.clientWidth || 0) + 'px';
         const miniSize = NOVA.aspectRatio.sizeToFit({
            'srcWidth': NOVA.videoElement.videoWidth,
            'srcHeight': NOVA.videoElement.videoHeight,
            'maxWidth': (window.innerWidth / user_settings.player_float_scroll_size_ratio),
            'maxHeight': (window.innerHeight / user_settings.player_float_scroll_size_ratio),
         });
         let initcss = {
            width: NOVA.aspectRatio.calculateWidth(
               miniSize.height,
               NOVA.aspectRatio.chooseAspectRatio({ 'width': miniSize.width, 'height': miniSize.height })
            ) + 'px',
            height: miniSize.height + 'px',
            position: 'fixed',
            'z-index': 'var(--zIndex)',
            'box-shadow': '0 16px 24px 2px rgba(0, 0, 0, 0.14),' +
               '0 6px 30px 5px rgba(0, 0, 0, 0.12),' +
               '0 8px 10px -5px rgba(0, 0, 0, 0.4)',
         };
         switch (user_settings.player_float_scroll_position) {
            case 'top-left':
               initcss.top = user_settings['header-unfixed'] ? 0
                  : (document.getElementById('masthead-container')?.offsetHeight || 0) + 'px';
               initcss.left = 0;
               break;
            case 'top-right':
               initcss.top = user_settings['header-unfixed'] ? 0
                  : (document.getElementById('masthead-container')?.offsetHeight || 0) + 'px';
               initcss.right = scrollbarWidth;
               break;
            case 'bottom-left':
               initcss.bottom = 0;
               initcss.left = 0;
               break;
            case 'bottom-right':
               initcss.bottom = 0;
               initcss.right = scrollbarWidth;
               break;
         }
         NOVA.css.push(initcss, PINNED_SELECTOR, 'important');
         NOVA.css.push(
            PINNED_SELECTOR + `{
               --height: ${initcss.height} !important;
               --width: ${initcss.width} !important;
               width: var(--width) !important;
               height: var(--height) !important;
               background-color: var(--yt-spec-base-background);
            }
            ${PINNED_SELECTOR} video {
               object-fit: contain !important;
            }
            ${PINNED_SELECTOR} .ytp-chrome-controls .nova-right-custom-button,
            ${PINNED_SELECTOR} .ytp-chrome-controls #nova-player-time-remaining,
            ${PINNED_SELECTOR} .ytp-chrome-controls button.ytp-size-button,
            ${PINNED_SELECTOR} .ytp-chrome-controls button.ytp-subtitles-button,
            ${PINNED_SELECTOR} .ytp-chrome-controls button.ytp-settings-button,
            ${PINNED_SELECTOR} .ytp-chrome-controls .ytp-chapter-container {
               display: none !important;
            }`);
         NOVA.css.push(`
            ${PINNED_SELECTOR} .ytp-preview,
            ${PINNED_SELECTOR} .ytp-scrubber-container,
            ${PINNED_SELECTOR} .ytp-hover-progress,
            ${PINNED_SELECTOR} .ytp-gradient-bottom { display:none !important; }
            ${PINNED_SELECTOR} .ytp-chrome-bottom { width: 96% !important; }
            ${PINNED_SELECTOR} .ytp-chapters-container { display: flex; }`);
         NOVA.css.push(
            `${PINNED_SELECTOR} video {
               width: var(--width) !important;
               height: var(--height) !important;
               left: 0 !important;
               top: 0 !important;
            }
            .ended-mode video {
               visibility: hidden;
            }`);
      }
      function insertUnpinButton(player = movie_player) {
         NOVA.css.push(
            PINNED_SELECTOR + ` {
               --zIndex: ${1 + Math.max(
               NOVA.css.getValue('#chat', 'z-index'),
               NOVA.css.getValue('.ytp-chrome-top .ytp-cards-button', 'z-index'),
               NOVA.css.getValue('#chat', 'z-index'),
               601)};
            }
            ${UNPIN_BTN_SELECTOR} { display: none; }
            ${PINNED_SELECTOR} ${UNPIN_BTN_SELECTOR} {
               display: initial !important;
               position: absolute;
               cursor: pointer;
               top: 10px;
               left: 10px;
               width: 28px;
               height: 28px;
               color: white;
               border: none;
               outline: none;
               opacity: .1;
               z-index: var(--zIndex);
               font-size: 24px;
               font-weight: bold;
               background-color: rgba(0, 0, 0, 0.8);
            }
            ${PINNED_SELECTOR}:hover ${UNPIN_BTN_SELECTOR} { opacity: .7; }
            ${UNPIN_BTN_SELECTOR}:hover { opacity: 1 !important; }`);
         const btnUnpin = document.createElement('button');
         btnUnpin.className = UNPIN_BTN_CLASS_VALUE;
         btnUnpin.title = 'Unpin player';
         btnUnpin.textContent = '×';
         btnUnpin.addEventListener('click', () => {
            player.classList.remove(CLASS_VALUE);
            drag.reset('clear storePos');
            window.dispatchEvent(new Event('resize'));
         });
         player.append(btnUnpin);
         document.addEventListener('yt-navigate-start', () => {
            if (player.classList.contains(CLASS_VALUE)) {
               player.classList.remove(CLASS_VALUE);
               drag.reset();
            }
         });
      }
      const drag = {
         attrNametoLock: 'force_fix_preventDefault',
         reset(clear_storePos) {
            this.dragTarget?.style.removeProperty('transform');
            if (clear_storePos) this.storePos = this.xOffset = this.yOffset = 0;
            else this.storePos = { 'X': this.xOffset, 'Y': this.yOffset };
         },
         init(el_target = required()) {
            this.log('drag init', ...arguments);
            if (!(el_target instanceof HTMLElement)) return console.error('el_target not HTMLElement:', el_target);
            this.dragTarget = el_target;
            document.addEventListener('mousedown', evt => {
               if (!el_target.classList.contains(CLASS_VALUE)) return;
               this.dragStart.apply(this, [evt]);
            });
            document.addEventListener('mouseup', evt => {
               if (this.active) this.dragTarget.removeAttribute(this.attrNametoLock);
               this.dragEnd.apply(this, [evt]);
            });
            document.addEventListener('mousemove', evt => {
               if (this.active && !this.dragTarget.hasAttribute(this.attrNametoLock)) {
                  this.dragTarget.setAttribute(this.attrNametoLock, true);
               }
               this.draging.apply(this, [evt]);
            });
            NOVA.css.push(
               `[${this.attrNametoLock}]:active {
                  pointer-events: none;
               }`);
         },
         dragStart(evt) {
            if (!this.dragTarget.contains(evt.target)) return;
            this.log('dragStart');
            switch (evt.type) {
               case 'touchstart':
                  this.initialX = evt.touches[0].clientX - (this.xOffset || 0);
                  this.initialY = evt.touches[0].clientY - (this.yOffset || 0);
                  break;
               case 'mousedown':
                  this.initialX = evt.clientX - (this.xOffset || 0);
                  this.initialY = evt.clientY - (this.yOffset || 0);
                  break;
            }
            this.active = true;
            document.body.style.cursor = 'move';
         },
         dragEnd(evt) {
            if (!this.active) return;
            this.log('dragEnd');
            this.initialX = this.currentX;
            this.initialY = this.currentY;
            this.active = false;
            document.body.style.cursor = 'default';
         },
         draging(evt) {
            if (!this.active) return;
            evt.preventDefault();
            evt.stopImmediatePropagation();
            this.log('draging');
            switch (evt.type) {
               case 'touchmove':
                  this.currentX = evt.touches[0].clientX - this.initialX;
                  this.currentY = evt.touches[0].clientY - this.initialY;
                  break;
               case 'mousemove':
                  const
                     rect = this.dragTarget.getBoundingClientRect();
                  if (rect.left >= document.body.clientWidth - this.dragTarget.offsetWidth) {
                     this.currentX = Math.min(
                        evt.clientX - this.initialX,
                        document.body.clientWidth - this.dragTarget.offsetWidth - this.dragTarget.offsetLeft
                     );
                  }
                  else {
                     this.currentX = Math.max(evt.clientX - this.initialX, 0 - this.dragTarget.offsetLeft);
                  }
                  if (rect.top >= window.innerHeight - this.dragTarget.offsetHeight) {
                     this.currentY = Math.min(
                        evt.clientY - this.initialY,
                        window.innerHeight - this.dragTarget.offsetHeight - this.dragTarget.offsetTop
                     );
                  }
                  else {
                     this.currentY = Math.max(evt.clientY - this.initialY, 0 - this.dragTarget.offsetTop);
                  }
                  break;
            }
            this.xOffset = this.currentX;
            this.yOffset = this.currentY;
            this.setTranslate({ 'X': this.currentX, 'Y': this.currentY });
         },
         setTranslate({ X = required(), Y = required() }) {
            this.log('setTranslate', ...arguments);
            this.dragTarget.style.transform = `translate3d(${X}px, ${Y}px, 0)`;
         },
         log() {
            if (this.DEBUG && arguments.length) {
               console.groupCollapsed(...arguments);
               console.trace();
               console.groupEnd();
            }
         },
      };
   },
});
window.nova_plugins.push({
   id: 'video-quality',
   run_on_pages: 'watch, embed',
   _runtime: user_settings => {
      const qualityFormatListWidth = {
         highres: 4320,
         hd2880: 2880,
         hd2160: 2160,
         hd1440: 1440,
         hd1080: 1080,
         hd720: 720,
         large: 480,
         medium: 360,
         small: 240,
         tiny: 144,
      };
      let selectedQuality = user_settings.video_quality;
      NOVA.waitSelector('#movie_player')
         .then(movie_player => {
            if (user_settings.video_quality_manual_save_in_tab
               && NOVA.currentPage == 'watch'
            ) {
               movie_player.addEventListener('onPlaybackQualityChange', quality => {
                  if (document.activeElement.getAttribute('role') == 'menuitemradio'
                     && quality !== selectedQuality
                  ) {
                     console.info(`keep quality "${quality}" in the session`);
                     selectedQuality = quality;
                     user_settings.video_quality_in_music_playlist = false;
                  }
               });
            }
            if (user_settings['save-channel-state']) {
               NOVA.runOnPageInitOrTransition(async () => {
                  if (NOVA.currentPage == 'watch' && (userQuality = await NOVA.storage_obj_manager.getParam('quality'))) {
                     selectedQuality = userQuality;
                  }
               });
            }
            setQuality();
            movie_player.addEventListener('onStateChange', setQuality);
         });
      function setQuality(state) {
         if (!selectedQuality) return console.error('selectedQuality unavailable', selectedQuality);
         if (user_settings.video_quality_in_music_playlist
            && location.search.includes('list=')
            && NOVA.isMusic()
         ) {
            selectedQuality = user_settings.video_quality_in_music_quality;
         }
         if (['PLAYING', 'BUFFERING'].includes(NOVA.getPlayerState(state)) && !setQuality.quality_busy) {
            setQuality.quality_busy = true;
            const waitQuality = setInterval(() => {
               let availableQualityLevels = movie_player.getAvailableQualityLevels();
               const maxWidth = (NOVA.currentPage == 'watch') ? window.screen.width : window.innerWidth;
               const maxQualityIdx = availableQualityLevels
                  .findIndex(i => qualityFormatListWidth[i] <= (maxWidth * 1.3));
               availableQualityLevels = availableQualityLevels.slice(maxQualityIdx);
               if (availableQualityLevels?.length) {
                  clearInterval(waitQuality);
                  const maxAvailableQuality = Math.max(availableQualityLevels.indexOf(selectedQuality), 0);
                  const newQuality = availableQualityLevels[maxAvailableQuality];
                  if (movie_player.hasOwnProperty('setPlaybackQuality')) {
                     movie_player.setPlaybackQuality(newQuality);
                  }
                  if (movie_player.hasOwnProperty('setPlaybackQualityRange')) {
                     movie_player.setPlaybackQualityRange(newQuality, newQuality);
                  }
               }
            }, 50);
         }
         else if (state <= 0) {
            setQuality.quality_busy = false;
         }
      }
      NOVA.waitSelector('.ytp-error [class*="reason"]')
         .then(error_reason_el => {
            if (alertText = error_reason_el.textContent) {
               throw alertText;
            }
         });
   },
});
window.nova_plugins.push({
   id: 'player-resume-playback',
   run_on_pages: 'watch, embed',
   _runtime: user_settings => {
      if (!navigator.cookieEnabled && NOVA.currentPage == 'embed') return;
      const
         CACHE_PREFIX = 'nova-resume-playback-time',
         getCacheName = () => CACHE_PREFIX + ':' + (NOVA.queryURL.get('v') || movie_player.getVideoData().video_id);
      let cacheName;
      NOVA.waitSelector('video')
         .then(video => {
            cacheName = getCacheName();
            resumePlayback.apply(video);
            video.addEventListener('loadeddata', resumePlayback.bind(video));
            video.addEventListener('timeupdate', savePlayback.bind(video));
            video.addEventListener('ended', () => sessionStorage.removeItem(cacheName));
            if (user_settings.player_resume_playback_url_mark && NOVA.currentPage != 'embed') {
               if (NOVA.queryURL.has('t')) {
                  document.addEventListener('yt-navigate-finish',
                     connectSaveStateInURL.bind(video), { capture: true, once: true });
               }
               else {
                  connectSaveStateInURL.apply(video);
               }
            }
         });
      function savePlayback() {
         if (this.currentTime > 5 && this.duration > 30 && !movie_player.classList.contains('ad-showing')) {
            sessionStorage.setItem(cacheName, ~~this.currentTime);
         }
      }
      async function resumePlayback() {
         if (NOVA.queryURL.has('t')
            || (user_settings['save-channel-state'] && await NOVA.storage_obj_manager.getParam('ignore-playback'))
         ) {
            return;
         }
         cacheName = getCacheName();
         if ((time = +sessionStorage.getItem(cacheName))
            && (time < (this.duration - 1))
         ) {
            this.currentTime = time;
         }
      }
      function connectSaveStateInURL() {
         let delaySaveOnPauseURL;
         this.addEventListener('pause', () => {
            if (this.currentTime < (this.duration - 1) && this.currentTime > 5 && this.duration > 10) {
               delaySaveOnPauseURL = setTimeout(() => {
                  NOVA.updateUrl(NOVA.queryURL.set({ 't': ~~this.currentTime + 's' }));
               }, 100);
            }
         });
         this.addEventListener('play', () => {
            if (typeof delaySaveOnPauseURL === 'number') clearTimeout(delaySaveOnPauseURL);
            if (NOVA.queryURL.has('t')) NOVA.updateUrl(NOVA.queryURL.remove('t'));
         });
      }
   },
});
window.nova_plugins.push({
   id: 'video-stop-preload',
   run_on_pages: 'watch, embed',
   _runtime: user_settings => {
      if (user_settings.stop_preload_embed && NOVA.currentPage != 'embed') return;
      if (location.hostname == 'youtube.googleapis.com') return;
      if (NOVA.currentPage == 'embed'
         && window.self !== window.top
         && ['0', 'false'].includes(NOVA.queryURL.get('autoplay'))
      ) {
         return;
      }
      NOVA.waitSelector('#movie_player')
         .then(async movie_player => {
            let disableStop;
            document.addEventListener('yt-navigate-start', () => disableStop = false);
            await NOVA.waitUntil(() => typeof movie_player === 'object' && typeof movie_player.stopVideo === 'function');
            movie_player.stopVideo();
            movie_player.addEventListener('onStateChange', onPlayerStateChange.bind(this));
            function onPlayerStateChange(state) {
               if (user_settings.stop_preload_ignore_playlist && location.search.includes('list=')) return;
               if (user_settings.stop_preload_ignore_live && movie_player.getVideoData().isLive) return;
               if (!disableStop && state > 0 && state < 5) {
                  movie_player.stopVideo();
               }
            }
            document.addEventListener('click', disableHoldStop);
            document.addEventListener('keyup', ({ code }) => (code == 'Space') && disableHoldStop());
            function disableHoldStop() {
               if (!disableStop && movie_player.contains(document.activeElement)) {
                  disableStop = true;
                  movie_player.playVideo();
               }
            }
         });
   },
});
window.nova_plugins.push({
   id: 'subtitle-style',
   run_on_pages: 'watch, embed, -mobile',
   _runtime: user_settings => {
      const SELECTOR = '.ytp-caption-segment';
      let css = {}
      if (user_settings.subtitle_transparent) {
         css = {
            'background': 'Transparent',
            'text-shadow':
               `rgb(0, 0, 0) 0 0 .1em,
               rgb(0, 0, 0) 0 0 .2em,
               rgb(0, 0, 0) 0 0 .4em`,
         };
      }
      if (user_settings.subtitle_bold) css['font-weight'] = 'bold';
      if (user_settings.subtitle_fixed) {
         NOVA.css.push(
            `.caption-window {
               margin-bottom: 1px !important;
               bottom: 1% !important;
            }`);
      }
      if (user_settings.subtitle_selectable) {
         NOVA.watchElements({
            selectors: [
               SELECTOR,
               '#caption-window-1',
            ]
               .map(i => i + ':not(:empty)'),
            callback: el => {
               el.addEventListener('mousedown', evt => evt.stopPropagation(), true);
               el.setAttribute('draggable', 'false');
               el.setAttribute('selectable', 'true');
               el.style.userSelect = 'text';
               el.style.WebkitUserSelect = 'text';
               el.style.cursor = 'text';
            }
         });
      }
      if (Object.keys(css).length) {
         NOVA.css.push(css, SELECTOR, 'important');
      }
   },
});
window.nova_plugins.push({
   id: 'video-unblock-region',
   run_on_pages: 'watch, -mobile',
   _runtime: user_settings => {
      NOVA.waitSelector('ytd-watch-flexy[player-unavailable]')
         .then(el => el.querySelector('yt-player-error-message-renderer #button.yt-player-error-message-renderer button') || redirect());
      function redirect(new_tab_url) {
         if (new_tab_url) {
            window.open(`${location.protocol}//${user_settings.video_unblock_region_domain || 'hooktube.com'}${location.port ? ':' + location.port : ''}/watch?v=` + movie_player.getVideoData().video_id);
         }
         else {
            location.hostname = user_settings.video_unblock_region_domain || 'hooktube.com';
         }
         if (user_settings.video_unblock_region_open_map) {
            window.open(`https://watannetwork.com/tools/blocked/#url=${NOVA.queryURL.get('v')}:~:text=Allowed%20countries`);
         }
      }
   },
});
window.nova_plugins.push({
   id: 'player-quick-buttons',
   run_on_pages: 'watch, embed, -mobile',
   _runtime: user_settings => {
      const
         SELECTOR_BTN_CLASS_NAME = 'nova-right-custom-button',
         SELECTOR_BTN = '.' + SELECTOR_BTN_CLASS_NAME;
      NOVA.waitSelector('.ytp-right-controls')
         .then(async container => {
            NOVA.videoElement = await NOVA.waitSelector('video');
            NOVA.css.push(
               `${SELECTOR_BTN} {
                  user-select: none;
               }
               ${SELECTOR_BTN}:hover { color: #66afe9 !important; }
               ${SELECTOR_BTN}:active { color: #2196f3 !important; }`);
            NOVA.css.push(
               `${SELECTOR_BTN}[tooltip]:hover::before {
                  content: attr(tooltip);
                  position: absolute;
                  top: -3em;
                  transform: translateX(-30%);
                  line-height: normal;
                  background-color: rgba(28,28,28,.9);
                  border-radius: 2px;
                  padding: 5px 9px;
                  color: #fff;
                  font-weight: bold;
                  white-space: nowrap;
               }
               html[data-cast-api-enabled] ${SELECTOR_BTN}[tooltip]:hover::before {
                  font-weight: normal;
               }`);
            if (user_settings.player_buttons_custom_items?.includes('picture-in-picture')) {
               const pipBtn = document.createElement('button');
               pipBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
               pipBtn.setAttribute('tooltip', 'Open in PictureInPicture');
               pipBtn.innerHTML = createSVG();
               pipBtn.addEventListener('click', () => document.pictureInPictureElement
                  ? document.exitPictureInPicture() : NOVA.videoElement.requestPictureInPicture()
               );
               container.prepend(pipBtn);
               NOVA.videoElement?.addEventListener('enterpictureinpicture', () => pipBtn.innerHTML = createSVG(2));
               NOVA.videoElement?.addEventListener('leavepictureinpicture', () => pipBtn.innerHTML = createSVG());
               function createSVG(alt) {
                  const svg = document.createElement('svg');
                  svg.setAttribute('width', '100%');
                  svg.setAttribute('height', '100%');
                  svg.setAttribute('viewBox', '-8 -6 36 36');
                  const path = document.createElement('path');
                  path.setAttribute('fill', 'currentColor');
                  path.setAttribute('d', alt
                     ? 'M18.5,11H18v1h.5A1.5,1.5,0,0,1,20,13.5v5A1.5,1.5,0,0,1,18.5,20h-8A1.5,1.5,0,0,1,9,18.5V18H8v.5A2.5,2.5,0,0,0,10.5,21h8A2.5,2.5,0,0,0,21,18.5v-5A2.5,2.5,0,0,0,18.5,11Z M14.5,4H2.5A2.5,2.5,0,0,0,0,6.5v8A2.5,2.5,0,0,0,2.5,17h12A2.5,2.5,0,0,0,17,14.5v-8A2.5,2.5,0,0,0,14.5,4Z'
                     : 'M2.5,17A1.5,1.5,0,0,1,1,15.5v-9A1.5,1.5,0,0,1,2.5,5h13A1.5,1.5,0,0,1,17,6.5V10h1V6.5A2.5,2.5,0,0,0,15.5,4H2.5A2.5,2.5,0,0,0,0,6.5v9A2.5,2.5,0,0,0,2.5,18H7V17Z M18.5,11h-8A2.5,2.5,0,0,0,8,13.5v5A2.5,2.5,0,0,0,10.5,21h8A2.5,2.5,0,0,0,21,18.5v-5A2.5,2.5,0,0,0,18.5,11Z');
                  svg.append(path);
                  return svg.outerHTML;
               }
            }
            if (user_settings.player_buttons_custom_items?.indexOf('popup') !== -1 && !NOVA.queryURL.has('popup')) {
               const popupBtn = document.createElement('button');
               popupBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
               popupBtn.setAttribute('tooltip', 'Open in popup');
               popupBtn.innerHTML =
                  `<svg viewBox="-8 -8 36 36" height="100%" width="100%">
                     <g fill="currentColor">
                        <path d="M18 2H6v4H2v12h12v-4h4V2z M12 16H4V8h2v6h6V16z M16 12h-2h-2H8V8V6V4h8V12z" />
                     </g>
                  </svg>`;
               popupBtn.addEventListener('click', () => {
                  const
                     width = screen.width / (+user_settings.player_buttons_custom_popup_width || 4),
                     aspectRatio = NOVA.aspectRatio.getAspectRatio({ 'width': NOVA.videoElement.videoWidth, 'height': NOVA.videoElement.videoHeight }),
                     height = Math.round(width / aspectRatio);
                  url = new URL(
                     document.querySelector('link[itemprop="embedUrl"][href]')?.href
                     || (location.origin + '/embed/' + movie_player.getVideoData().video_id)
                  );
                  if (currentTime = ~~NOVA.videoElement?.currentTime) url.searchParams.set('start', currentTime);
                  url.searchParams.set('autoplay', 1);
                  url.searchParams.set('popup', true);
                  openPopup({ 'url': url.href, 'title': document.title, 'width': width, 'height': height });
               });
               container.prepend(popupBtn);
               function openPopup({ url, title, width, height }) {
                  const left = (screen.width / 2) - (width / 2);
                  const top = (screen.height / 2) - (height / 2);
                  const newWindow = window.open(url, '_blank', `popup=1,toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=${width},height=${height},top=${top},left=${left}`);
                  return;
               }
            }
            if (user_settings.player_buttons_custom_items?.includes('screenshot')) {
               const
                  SELECTOR_SCREENSHOT_ID = 'nova-screenshot-result',
                  SELECTOR_SCREENSHOT = '#' + SELECTOR_SCREENSHOT_ID;
               NOVA.css.push(
                  SELECTOR_SCREENSHOT + ` {
                     --width: 400px;
                     --height: 400px;
                     position: fixed;
                     top: 0;
                     right: 0;
                     overflow: hidden;
                     margin: 36px 30px;
                     box-shadow: 0 0 15px #000;
                     max-width: var(--width);
                     max-height: var(--height);
                  }
                  ${SELECTOR_SCREENSHOT} canvas {
                     max-width: var(--width);
                     max-height: var(--height);
                  }
                  ${SELECTOR_SCREENSHOT} .close-btn {
                     position: absolute;
                     bottom: 0;
                     right: 0;
                     background-color: rgba(0, 0, 0, .5);
                     color: #FFF;
                     cursor: pointer;
                     font-size: 12px;
                     display: grid;
                     height: 100%;
                     width: 25%;
                  }
                  ${SELECTOR_SCREENSHOT} .close-btn:hover { background-color: rgba(0, 0, 0, .65); }
                  ${SELECTOR_SCREENSHOT} .close-btn > * { margin: auto; }`);
               const screenshotBtn = document.createElement('button');
               screenshotBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
               screenshotBtn.setAttribute('tooltip', 'Take screenshot');
               screenshotBtn.innerHTML =
                  `<svg viewBox="0 -166 512 860" height="100%" width="100%">
                     <g fill="currentColor">
                        <circle cx="255.811" cy="285.309" r="75.217" />
                        <path d="M477,137H352.718L349,108c0-16.568-13.432-30-30-30H191c-16.568,0-30,13.432-30,30l-3.718,29H34 c-11.046,0-20,8.454-20,19.5v258c0,11.046,8.954,20.5,20,20.5h443c11.046,0,20-9.454,20-20.5v-258C497,145.454,488.046,137,477,137 z M255.595,408.562c-67.928,0-122.994-55.066-122.994-122.993c0-67.928,55.066-122.994,122.994-122.994 c67.928,0,122.994,55.066,122.994,122.994C378.589,353.495,323.523,408.562,255.595,408.562z M474,190H369v-31h105V190z" />
                     </g>
                  </svg>`;
               screenshotBtn.addEventListener('click', () => {
                  const
                     container = document.getElementById(SELECTOR_SCREENSHOT_ID) || document.createElement('a'),
                     canvas = container.querySelector('canvas') || document.createElement('canvas');
                  canvas.width = NOVA.videoElement.videoWidth;
                  canvas.height = NOVA.videoElement.videoHeight
                  canvas.getContext('2d').drawImage(NOVA.videoElement, 0, 0, canvas.width, canvas.height);
                  canvas.title = 'Click to save';
                  try {
                     canvas.toBlob(blob => container.href = URL.createObjectURL(blob));
                  } catch (error) {
                  }
                  if (!container.id) {
                     container.id = SELECTOR_SCREENSHOT_ID;
                     container.target = '_blank';
                     if (headerContainer = document.getElementById('masthead-container')) {
                        container.style.marginTop = (headerContainer?.offsetHeight || 0) + 'px';
                        container.style.zIndex = +getComputedStyle(headerContainer)['z-index'] + 1;
                     }
                     canvas.addEventListener('click', evt => {
                        evt.preventDefault();
                        downloadCanvasAsImage(evt.target);
                        container.remove();
                     });
                     container.append(canvas);
                     const close = document.createElement('a');
                     close.className = 'close-btn'
                     close.innerHTML = '<span>CLOSE</span>';
                     close.title = 'Close';
                     close.addEventListener('click', evt => {
                        evt.preventDefault();
                        container.remove();
                     });
                     container.append(close);
                     document.body.append(container);
                  }
               });
               function downloadCanvasAsImage(canvas) {
                  const
                     downloadLink = document.createElement('a'),
                     downloadFileName =
                        [
                           movie_player.getVideoData().title
                              .replace(/[\\/:*?"<>|]+/g, '')
                              .replace(/\s+/g, ' ').trim(),
                           `[${NOVA.timeFormatTo.HMS.abbr(NOVA.videoElement.currentTime)}]`,
                        ]
                           .join(' ');
                  downloadLink.href = canvas.toBlob(blob => URL.createObjectURL(blob));
                  downloadLink.download = downloadFileName + '.png';
                  downloadLink.click();
               }
               container.prepend(screenshotBtn);
            }
            if (user_settings.player_buttons_custom_items?.includes('thumbnail')) {
               const thumbBtn = document.createElement('button');
               thumbBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
               thumbBtn.setAttribute('tooltip', 'View Thumbnail');
               thumbBtn.innerHTML =
                  `<svg viewBox="0 -10 21 40" height="100%" width="100%">
                     <g fill="currentColor">
                        <circle cx='8' cy='7.2' r='2'/>
                        <path d='M0 2v16h20V2H0z M18 16H2V4h16V16z'/>
                        <polygon points='17 10.9 14 7.9 9 12.9 6 9.9 3 12.9 3 15 17 15' />
                     </g>
                  </svg>`;
               thumbBtn.addEventListener('click', async () => {
                  const
                     videoId = movie_player.getVideoData().video_id || NOVA.queryURL.get('v'),
                     thumbsSizesTemplate = [
                        'maxres',
                        'sd',
                        'hq',
                        'mq',
                        ''
                     ];
                  document.body.style.cursor = 'wait';
                  for (const resPrefix of thumbsSizesTemplate) {
                     const
                        imgUrl = `https://i.ytimg.com/vi/${videoId}/${resPrefix}default.jpg`,
                        response = await fetch(imgUrl);
                     if (response.status === 200) {
                        document.body.style.cursor = 'default';
                        window.open(imgUrl);
                        break;
                     }
                  }
               });
               container.prepend(thumbBtn);
            }
            if (user_settings.player_buttons_custom_items?.includes('rotate')) {
               const rotateBtn = document.createElement('button');
               rotateBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
               rotateBtn.setAttribute('tooltip', 'Rotate video');
               Object.assign(rotateBtn.style, {
                  padding: '0 1.1em',
               });
               rotateBtn.innerHTML =
                  `<svg viewBox="0 0 1536 1536" height="100%" width="100%">
                     <g fill="currentColor">
                        <path
                           d="M1536 128v448q0 26-19 45t-45 19h-448q-42 0-59-40-17-39 14-69l138-138Q969 256 768 256q-104 0-198.5 40.5T406 406 296.5 569.5 256 768t40.5 198.5T406 1130t163.5 109.5T768 1280q119 0 225-52t179-147q7-10 23-12 14 0 25 9l137 138q9 8 9.5 20.5t-7.5 22.5q-109 132-264 204.5T768 1536q-156 0-298-61t-245-164-164-245T0 768t61-298 164-245T470 61 768 0q147 0 284.5 55.5T1297 212l130-129q29-31 70-14 39 17 39 59z"/>
                        </path>
                     </g>
                  </svg>`;
               rotateBtn.addEventListener('click', () => {
                  let angle = parseInt(NOVA.videoElement.style.transform.replace(/\D+/, '')) || 0;
                  const scale = (angle === 0 || angle === 180) ? movie_player.clientHeight / NOVA.videoElement.clientWidth : 1;
                  angle += 90;
                  NOVA.videoElement.style.transform = (angle === 360) ? '' : `rotate(${angle}deg) scale(${scale})`;
               });
               container.prepend(rotateBtn);
            }
            if (user_settings.player_buttons_custom_items?.includes('aspect-ratio')) {
               const
                  aspectRatioBtn = document.createElement('a'),
                  aspectRatioList = [
                     { '16:9': 1.335 },
                     { '4:3': .75 },
                     { '9:16': 1.777777778 },
                     { 'auto': 1 },
                  ],
                  genTooltip = (key = 0) => `Switch aspect ratio to ` + Object.keys(aspectRatioList[key]);
               aspectRatioBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
               aspectRatioBtn.style.textAlign = 'center';
               aspectRatioBtn.style.fontWeight = 'bold';
               aspectRatioBtn.setAttribute('tooltip', genTooltip());
               aspectRatioBtn.innerHTML = '1:1';
               aspectRatioBtn.addEventListener('click', () => {
                  if (!NOVA.videoElement) return;
                  const getNextIdx = () => (this.listIdx < aspectRatioList.length - 1) ? this.listIdx + 1 : 0;
                  this.listIdx = getNextIdx();
                  NOVA.videoElement.style.transform = `scaleX(${Object.values(aspectRatioList[this.listIdx])})`;
                  aspectRatioBtn.setAttribute('tooltip', genTooltip(getNextIdx()));
                  aspectRatioBtn.textContent = Object.keys(aspectRatioList[this.listIdx]);
               });
               container.prepend(aspectRatioBtn);
            }
            if (user_settings.player_buttons_custom_items?.includes('watch-later')) {
               NOVA.waitSelector('.ytp-watch-later-button')
                  .then(watchLaterDefault => {
                     NOVA.css.push(
                        `.${SELECTOR_BTN_CLASS_NAME} .ytp-spinner-container {
                           position: relative;
                           top: 0;
                           left: 0;
                           scale: .5;
                           margin: 0;
                        }
                        .${SELECTOR_BTN_CLASS_NAME}.watch-later-btn svg {
                           scale: .85;
                        }`);
                     const watchLaterBtn = document.createElement('button');
                     watchLaterBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME} watch-later-btn`;
                     watchLaterBtn.setAttribute('tooltip', 'Watch later');
                     renderIcon();
                     watchLaterBtn.addEventListener('click', () => {
                        watchLaterDefault.click();
                        renderIcon();
                        const waitStatus = setInterval(() => {
                           if (watchLaterDefault.querySelector('svg')) {
                              clearInterval(waitStatus);
                              renderIcon();
                           }
                        }, 100);
                     });
                     [...document.getElementsByClassName(SELECTOR_BTN_CLASS_NAME)].pop()
                        ?.after(watchLaterBtn);
                     function renderIcon() {
                        watchLaterBtn.innerHTML = watchLaterDefault.querySelector('.ytp-watch-later-icon')?.innerHTML;
                     }
                  });
            }
            if (user_settings.player_buttons_custom_items?.includes('card-switch')
               && !user_settings.player_hide_elements?.includes('videowall_endscreen')
               && !user_settings.player_hide_elements?.includes('card_endscreen')
            ) {
               const
                  cardAttrName = 'nova-hide-endscreen',
                  cardBtn = document.createElement('button');
               NOVA.css.push(
                  `#movie_player[${cardAttrName}] .videowall-endscreen,
                  #movie_player[${cardAttrName}] .ytp-pause-overlay,
                  #movie_player[${cardAttrName}] [class^="ytp-ce-"] {
                     display: none !important;
                  }`);
               cardBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
               cardBtn.innerHTML = createSVG();
               if (user_settings.player_buttons_custom_card_switch) {
                  switchState(movie_player.toggleAttribute(cardAttrName));
               }
               cardBtn.addEventListener('click', () => switchState(movie_player.toggleAttribute(cardAttrName)));
               function switchState(state = required()) {
                  cardBtn.innerHTML = createSVG(state)
                  cardBtn.setAttribute('tooltip', `The cards are currently ${state ? 'hidden' : 'showing'}`);
               }
               function createSVG(alt) {
                  const svg = document.createElement('svg');
                  svg.setAttribute('width', '100%');
                  svg.setAttribute('height', '100%');
                  svg.setAttribute('viewBox', '-200 0 912 512');
                  const g = document.createElement('g');
                  g.setAttribute('fill', 'currentColor');
                  g.innerHTML = alt
                     ? '<path d="M 409 57.104 C 407.625 57.641, 390.907 73.653, 371.848 92.687 L 337.196 127.293 323.848 120.738 C 301.086 109.561, 283.832 103.994, 265.679 101.969 C 217.447 96.591, 148.112 134.037, 59.026 213.577 C 40.229 230.361, 4.759 265.510, 2.089 270 C -0.440 274.252, -0.674 281.777, 1.575 286.516 C 4.724 293.153, 67.054 352.112, 89.003 369.217 L 92.490 371.934 63.330 401.217 C 37.873 426.781, 34.079 430.988, 33.456 434.346 C 31.901 442.720, 38.176 452.474, 46.775 455.051 C 56.308 457.907, 41.359 471.974, 244.317 269.173 C 350.152 163.421, 429.960 82.914, 431.067 80.790 C 436.940 69.517, 428.155 55.840, 415.185 56.063 C 413.158 56.098, 410.375 56.566, 409 57.104 M 245.500 137.101 C 229.456 139.393, 201.143 151.606, 177.500 166.433 C 151.339 182.839, 120.778 206.171, 89.574 233.561 C 72.301 248.723, 42 277.649, 42 278.977 C 42 280.637, 88.281 323.114, 108.367 339.890 L 117.215 347.279 139.209 325.285 L 161.203 303.292 159.601 293.970 C 157.611 282.383, 157.570 272.724, 159.465 261.881 C 165.856 225.304, 193.011 195.349, 229.712 184.389 C 241.299 180.929, 261.648 179.996, 272.998 182.405 L 280.496 183.996 295.840 168.652 L 311.183 153.309 303.342 149.583 C 292.100 144.242, 277.007 139.186, 267.205 137.476 C 257.962 135.865, 254.565 135.806, 245.500 137.101 M 377.500 163.164 C 374.231 164.968, 369.928 169.297, 368.295 172.423 C 366.203 176.431, 366.351 184.093, 368.593 187.889 C 369.597 189.587, 375.944 195.270, 382.699 200.516 C 406.787 219.226, 444.129 252.203, 462.500 270.989 L 470.500 279.170 459 290.204 C 374.767 371.030, 302.827 418.200, 259.963 420.709 C 239.260 421.921, 213.738 412.918, 179.575 392.352 C 167.857 385.298, 166.164 384.571, 161.448 384.571 C 154.702 384.571, 149.091 388.115, 146.121 394.250 C 143.531 399.600, 143.472 403.260, 145.890 408.500 C 148.270 413.656, 150.468 415.571, 162 422.535 C 198.520 444.590, 230.555 455.992, 256 455.992 C 305.062 455.992, 376.663 414.097, 462 335.458 C 483.584 315.567, 509.652 289.051, 510.931 285.685 C 512.694 281.042, 512.218 273.876, 509.889 270 C 507.494 266.017, 484.252 242.741, 463.509 223.552 C 437.964 199.922, 398.967 167.566, 391.300 163.639 C 387.656 161.773, 380.470 161.526, 377.500 163.164 M 235.651 219.459 C 231.884 220.788, 226.369 223.351, 223.395 225.153 C 216.405 229.389, 206.759 239.019, 202.502 246.010 C 198.959 251.828, 193.677 266.197, 194.194 268.611 C 194.372 269.437, 205.637 258.890, 220.993 243.519 C 249.683 214.801, 249.910 214.427, 235.651 219.459 M 316.962 223.250 C 313.710 224.890, 311.876 226.720, 310.200 230 C 307.188 235.893, 307.781 240.006, 313.805 255 C 317.867 265.109, 318.470 267.589, 318.790 275.500 C 319.554 294.378, 313.786 309.236, 300.522 322.557 C 287.282 335.854, 274.164 341.408, 256 341.408 C 244.216 341.408, 238.392 340.027, 226.837 334.489 C 214.541 328.596, 204.996 330.563, 200.250 339.966 C 191.301 357.697, 210.339 372.220, 247.484 375.998 C 301.141 381.456, 350.063 339.760, 353.664 285.500 C 354.618 271.136, 351.039 249.928, 345.577 237.579 C 342.933 231.601, 337.061 224.600, 332.875 222.435 C 328.782 220.319, 322.095 220.661, 316.962 223.250" fill-rule="evenodd" />'
                     : `<path d="M 377.5 163.164 C 374.231 164.968 375.944 195.27 382.699 200.516 C 406.787 219.226 444.129 252.203 462.5 270.989 L 470.5 279.17 L 459 290.204 C 374.767 371.03 302.827 418.2 259.963 420.709 C 239.26 421.921 213.738 412.918 179.575 392.352 C 167.857 385.298 166.164 384.571 161.448 384.571 C 154.702 384.571 149.091 388.115 146.121 394.25 C 143.531 399.6 143.472 403.26 145.89 408.5 C 148.27 413.656 150.468 415.571 162 422.535 C 198.52 444.59 230.555 455.992 256 455.992 C 305.062 455.992 376.663 414.097 462 335.458 C 483.584 315.567 509.652 289.051 510.931 285.685 C 512.694 281.042 512.218 273.876 509.889 270 C 507.494 266.017 484.252 242.741 463.509 223.552 C 437.964 199.922 398.967 167.566 391.3 163.639 C 387.656 161.773 380.47 161.526 377.5 163.164 M 316.962 223.25 C 313.71 224.89 311.876 226.72 310.2 230 C 307.188 235.893 307.781 240.006 313.805 255 C 317.867 265.109 318.47 267.589 318.79 275.5 C 319.554 294.378 313.786 309.236 300.522 322.557 C 287.282 335.854 274.164 341.408 256 341.408 C 244.216 341.408 238.392 340.027 226.837 334.489 C 214.541 328.596 204.996 330.563 200.25 339.966 C 191.301 357.697 210.339 372.22 247.484 375.998 C 301.141 381.456 350.063 339.76 353.664 285.5 C 354.618 271.136 351.039 249.928 345.577 237.579 C 342.933 231.601 337.061 224.6 332.875 222.435 C 328.782 220.319 322.095 220.661 316.962 223.25"></path>
                     <path d="M 377.487 163.483 C 374.218 165.287 369.915 169.616 368.282 172.742 C 366.19 176.75 366.338 184.412 368.58 188.208 C 369.584 189.906 375.931 195.589 382.686 200.835 C 406.774 219.545 444.116 252.522 462.487 271.308 L 470.487 279.489 L 458.987 290.523 C 374.754 371.349 302.814 418.519 259.95 421.028 C 239.247 422.24 213.725 413.237 179.562 392.671 C 167.844 385.617 166.151 384.89 161.435 384.89 C 154.689 384.89 149.078 388.434 146.108 394.569 C 143.518 399.919 143.459 403.579 145.877 408.819 C 148.257 413.975 150.455 415.89 161.987 422.854 C 198.507 444.909 230.542 456.311 255.987 456.311 C 305.049 456.311 376.65 414.416 461.987 335.777 C 483.571 315.886 509.639 289.37 510.918 286.004 C 512.681 281.361 512.205 274.195 509.876 270.319 C 507.481 266.336 484.239 243.06 463.496 223.871 C 437.951 200.241 398.954 167.885 391.287 163.958 C 387.643 162.092 380.457 161.845 377.487 163.483 M 316.949 223.569 C 313.697 225.209 311.863 227.039 310.187 230.319 C 307.175 236.212 307.768 240.325 313.792 255.319 C 317.854 265.428 318.457 267.908 318.777 275.819 C 319.541 294.697 313.773 309.555 300.509 322.876 C 287.269 336.173 274.151 341.727 255.987 341.727 C 244.203 341.727 238.379 340.346 226.824 334.808 C 214.528 328.915 204.983 330.882 200.237 340.285 C 191.288 358.016 210.326 372.539 247.471 376.317 C 301.128 381.775 350.05 340.079 353.651 285.819 C 354.605 271.455 351.026 250.247 345.564 237.898 C 342.92 231.92 337.048 224.919 332.862 222.754 C 328.769 220.638 322.082 220.98 316.949 223.569" transform="matrix(-1, 0, 0, -1, 512.000305, 558.092285)"></path>`;
                  svg.append(g);
                  return svg.outerHTML;
               }
               container.prepend(cardBtn);
            }
            if (user_settings.player_buttons_custom_items?.includes('quick-quality')) {
               const
                  SELECTOR_QUALITY_CLASS_NAME = 'nova-quick-quality',
                  SELECTOR_QUALITY = '.' + SELECTOR_QUALITY_CLASS_NAME,
                  qualityContainerBtn = document.createElement('a'),
                  SELECTOR_QUALITY_LIST_ID = SELECTOR_QUALITY_CLASS_NAME + '-list',
                  SELECTOR_QUALITY_LIST = '#' + SELECTOR_QUALITY_LIST_ID,
                  listQuality = document.createElement('ul'),
                  SELECTOR_QUALITY_TITLE_ID = SELECTOR_QUALITY_CLASS_NAME + '-title',
                  qualityBtn = document.createElement('span'),
                  qualityFormatList = {
                     highres: { label: '4320p', badge: '8K' },
                     hd2880: { label: '2880p', badge: '5K' },
                     hd2160: { label: '2160p', badge: '4K' },
                     hd1440: { label: '1440p', badge: 'QHD' },
                     hd1080: { label: '1080p', badge: 'FHD' },
                     hd720: { label: '720p', badge: 'ᴴᴰ' },
                     large: { label: '480p' },
                     medium: { label: '360p' },
                     small: { label: '240p' },
                     tiny: { label: '144p' },
                     auto: { label: 'auto' },
                  };
               NOVA.css.push(
                  SELECTOR_QUALITY + ` {
                     overflow: visible !important;
                     position: relative;
                     text-align: center !important;
                     vertical-align: top;
                     font-weight: bold;
                  }
                  ${SELECTOR_QUALITY_LIST} {
                     position: absolute;
                     bottom: 2.5em !important;
                     left: -2.2em;
                     list-style: none;
                     padding-bottom: 1.5em !important;
                     z-index: ${1 + Math.max(NOVA.css.getValue('.ytp-progress-bar', 'z-index'), 31)};
                  }
                  html[data-cast-api-enabled] ${SELECTOR_QUALITY_LIST} {
                     margin: 0;
                     padding: 0;
                     bottom: 3.3em;
                  }
                  ${SELECTOR_QUALITY}:not(:hover) ${SELECTOR_QUALITY_LIST} {
                     display: none;
                  }
                  ${SELECTOR_QUALITY_LIST} li {
                     cursor: pointer;
                     white-space: nowrap;
                     line-height: 1.4;
                     background: rgba(28, 28, 28, 0.9);
                     margin: .3em 0;
                     padding: .5em 3em;
                     border-radius: .3em;
                     color: #fff;
                  }
                  ${SELECTOR_QUALITY_LIST} li .quality-menu-item-label-badge {
                     position: absolute;
                     right: 1em;
                     width: 1.7em;
                  }
                  ${SELECTOR_QUALITY_LIST} li.active { background: #720000; }
                  ${SELECTOR_QUALITY_LIST} li.disable { color: #666; }
                  ${SELECTOR_QUALITY_LIST} li:hover:not(.active) { background: #c00; }`);
               qualityContainerBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME} ${SELECTOR_QUALITY_CLASS_NAME}`;
               qualityBtn.id = SELECTOR_QUALITY_TITLE_ID;
               qualityBtn.textContent = qualityFormatList[movie_player.getPlaybackQuality()]?.label || '[out of range]';
               listQuality.id = SELECTOR_QUALITY_LIST_ID;
               movie_player.addEventListener('onPlaybackQualityChange', quality => {
                  document.getElementById(SELECTOR_QUALITY_TITLE_ID)
                     .textContent = qualityFormatList[quality]?.label || '[out of range]';
               });
               qualityContainerBtn.prepend(qualityBtn);
               qualityContainerBtn.append(listQuality);
               container.prepend(qualityContainerBtn);
               fillQualityMenu();
               NOVA.videoElement?.addEventListener('canplay', fillQualityMenu);
               function fillQualityMenu() {
                  if (qualityList = document.getElementById(SELECTOR_QUALITY_LIST_ID)) {
                     qualityList.innerHTML = '';
                     movie_player.getAvailableQualityLevels()
                        .forEach(quality => {
                           const qualityItem = document.createElement('li');
                           if (qualityData = qualityFormatList[quality]) {
                              qualityItem.textContent = qualityData.label;
                              if (badge = qualityData.badge) {
                                 qualityItem.insertAdjacentHTML('beforeend',
                                    `<span class="quality-menu-item-label-badge">${badge}</span>`);
                              }
                              if (movie_player.getPlaybackQuality() == quality) {
                                 qualityItem.className = 'active';
                              } else {
                                 const maxWidth = (NOVA.currentPage == 'watch') ? window.screen.width : window.innerWidth;
                                 if (+(qualityData.label.replace(/[^0-9]/g, '') || 0) <= (maxWidth * 1.3)) {
                                    qualityItem.addEventListener('click', () => {
                                       movie_player.setPlaybackQualityRange(quality, quality);
                                       if (quality == 'auto') return;
                                       qualityList.innerHTML = '';
                                    });
                                 }
                                 else {
                                    qualityItem.className = 'disable';
                                    qualityItem.title = 'Max (window viewport + 30%)';
                                 }
                              }
                              qualityList.append(qualityItem);
                           }
                        });
                  }
               }
            }
            if (user_settings.player_buttons_custom_items?.includes('clock')) {
               const clockEl = document.createElement('span');
               clockEl.className = 'ytp-time-display';
               clockEl.title = 'Now time';
               container.prepend(clockEl);
               setInterval(() => {
                  if (document.visibilityState == 'hidden'
                     || movie_player.classList.contains('ytp-autohide')
                  ) {
                     return;
                  }
                  const time = new Date().toTimeString().slice(0, 8);
                  clockEl.textContent = time;
               }, 1000);
            }
            if (user_settings.player_buttons_custom_items?.includes('toggle-speed')) {
               const
                  speedBtn = document.createElement('a'),
                  hotkey = user_settings.player_buttons_custom_hotkey_toggle_speed || 'a',
                  defaultRateText = '1x',
                  genTooltip = () => `Switch to ${NOVA.videoElement.playbackRate}>${speedBtn.textContent} (${hotkey})`;
               let rateOrig = {};
               speedBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
               speedBtn.style.textAlign = 'center';
               speedBtn.style.fontWeight = 'bold';
               speedBtn.innerHTML = defaultRateText;
               speedBtn.setAttribute('tooltip', genTooltip());
               document.addEventListener('keyup', evt => {
                  if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
                  if (evt.key === hotkey) {
                     switchRate();
                  }
               });
               speedBtn.addEventListener('click', switchRate);
               function switchRate() {
                  if (Object.keys(rateOrig).length) {
                     playerRate.set(rateOrig);
                     rateOrig = {};
                     speedBtn.innerHTML = defaultRateText;
                  }
                  else {
                     rateOrig = (movie_player && NOVA.videoElement.playbackRate % .25) === 0
                        ? { 'default': movie_player.getPlaybackRate() }
                        : { 'html5': NOVA.videoElement.playbackRate };
                     let resetRate = Object.assign({}, rateOrig);
                     resetRate[Object.keys(resetRate)[0]] = 1;
                     playerRate.set(resetRate);
                     speedBtn.textContent = rateOrig[Object.keys(rateOrig)[0]] + 'x';
                  }
                  speedBtn.setAttribute('tooltip', genTooltip());
               }
               const playerRate = {
                  set(obj) {
                     if (obj.hasOwnProperty('html5') || !movie_player) {
                        NOVA.videoElement.playbackRate = obj.html5;
                     }
                     else {
                        movie_player.setPlaybackRate(obj.default);
                     }
                  },
               };
               container.prepend(speedBtn);
               visibilitySwitch();
               NOVA.videoElement?.addEventListener('ratechange', visibilitySwitch);
               NOVA.videoElement?.addEventListener('loadeddata', () => {
                  rateOrig = {};
                  speedBtn.textContent = defaultRateText;
                  visibilitySwitch();
               });
               function visibilitySwitch() {
                  if (!Object.keys(rateOrig).length) {
                     speedBtn.style.display = (NOVA.videoElement?.playbackRate === 1) ? 'none' : '';
                  }
               }
            }
         });
   },
});
window.nova_plugins.push({
   id: 'player-float-progress-bar',
   run_on_pages: 'watch, embed, -mobile',
   _runtime: user_settings => {
      if (NOVA.currentPage == 'embed' && window.self.location.href.includes('live_stream')
      ) return;
      if (NOVA.currentPage == 'embed' && ['0', 'false'].includes(NOVA.queryURL.get('controls'))) return;
      const
         SELECTOR_ID = 'nova-player-float-progress-bar',
         SELECTOR = '#' + SELECTOR_ID,
         CHAPTERS_MARK_WIDTH_PX = '2px';
      NOVA.waitSelector('#movie_player.ytp-autohide video')
         .then(video => {
            const
               container = insertFloatBar(Math.max(
                  NOVA.css.getValue('.ytp-chrome-bottom', 'z-index'), 59
               ) + 1),
               bufferEl = document.getElementById(`${SELECTOR_ID}-buffer`),
               progressEl = document.getElementById(`${SELECTOR_ID}-progress`);
            renderChapters.init(video);
            video.addEventListener('loadeddata', resetBar);
            video.addEventListener('timeupdate', function () {
               if (notInteractiveToRender()) return;
               if (!isNaN(this.duration)) {
                  progressEl.style.transform = `scaleX(${this.currentTime / this.duration})`;
               }
            });
            video.addEventListener('progress', renderBuffer.bind(video));
            video.addEventListener('seeking', renderBuffer.bind(video));
            function renderBuffer() {
               if (notInteractiveToRender()) return;
               if ((totalDuration = movie_player.getDuration()) && !isNaN(totalDuration)) {
                  bufferEl.style.transform = `scaleX(${movie_player.getVideoLoadedFraction()})`;
               }
            }
            function resetBar() {
               container.style.display = movie_player.getVideoData().isLive ? 'none' : 'initial';
               container.classList.remove('transition');
               bufferEl.style.transform = 'scaleX(0)';
               progressEl.style.transform = 'scaleX(0)';
               container.classList.add('transition');
               renderChapters.init(video);
            }
            function notInteractiveToRender() {
               if (user_settings['player-control-below'] && NOVA.isFullscreen()) return;
               return (document.visibilityState == 'hidden'
                  || movie_player.getVideoData().isLive
               );
            }
         });
      function insertFloatBar(z_index = 60) {
         return document.getElementById(SELECTOR_ID) || (function () {
            movie_player.insertAdjacentHTML('beforeend',
               `<div id="${SELECTOR_ID}" class="transition">
                  <div class="container">
                     <div id="${SELECTOR_ID}-buffer" class="ytp-load-progress"></div>
                     <div id="${SELECTOR_ID}-progress" class="ytp-swatch-background-color"></div>
                  </div>
                  <div id="${SELECTOR_ID}-chapters"></div>
               </div>`);
            NOVA.css.push(
               `[id|=${SELECTOR_ID}] {
                  position: absolute;
                  bottom: 0;
               }
               ${SELECTOR} {
                  --opacity: ${+user_settings.player_float_progress_bar_opacity || .7};
                  --height: ${+user_settings.player_float_progress_bar_height || 3}px;
                  --bg-color: ${NOVA.css.getValue('.ytp-progress-list', 'background-color') || 'rgba(255,255,255,.2)'};
                  --zindex: ${z_index};
                  opacity: var(--opacity);
                  z-index: var(--zindex);
                  background-color: var(--bg-color);
                  width: 100%;
                  visibility: hidden;
               }
               #movie_player.ytp-autohide ${SELECTOR} {
                  visibility: visible;
               }
               ${SELECTOR}.transition [id|=${SELECTOR_ID}] {
                  transition: transform .2s linear;
               }
               ${SELECTOR}-progress, ${SELECTOR}-buffer {
                  width: 100%;
                  height: var(--height);
                  transform-origin: 0 0;
                  transform: scaleX(0);
               }
               ${SELECTOR}-progress {
                  z-index: calc(var(--zindex) + 1);
               }
               ${SELECTOR}-chapters {
                  position: relative;
                  width: 100%;
                  display: flex;
                  justify-content: flex-end;
               }
               ${SELECTOR}-chapters span {
                  height: var(--height);
                  z-index: calc(var(--zindex) + 1);
                  box-sizing: border-box;
                  padding: 0;
                  margin: 0;
               }
               ${SELECTOR}-chapters span:not(:first-child) {
                  border-left: ${CHAPTERS_MARK_WIDTH_PX} solid rgba(255,255,255,.7);
               }`);
            return document.getElementById(SELECTOR_ID);
         })();
      }
      const renderChapters = {
         async init(vid) {
            if (NOVA.currentPage == 'watch' && !(vid instanceof HTMLElement)) {
               return console.error('vid not HTMLElement:', chaptersContainer);
            }
            await NOVA.waitUntil(() => !isNaN(vid.duration), 1000);
            switch (NOVA.currentPage) {
               case 'watch':
                  this.from_description(vid.duration);
                  break;
               case 'embed':
                  await NOVA.waitUntil(() => (
                     chaptersContainer = document.body.querySelector('.ytp-chapters-container'))
                     && chaptersContainer?.children.length > 1
                     , 1000);
                  (
                     this.renderChaptersMarks(vid.duration)
                     || this.from_div(chaptersContainer)
                  );
                  break;
            }
         },
         from_description(duration = required()) {
            if (Math.sign(duration) !== 1) return console.error('duration not positive number:', duration);
            const selectorTimestampLink = 'a[href*="&t="]';
            NOVA.waitSelector(`ytd-watch-metadata #description.ytd-watch-metadata ${selectorTimestampLink}`)
               .then(() => this.renderChaptersMarks(duration));
            NOVA.waitSelector(`#comments #comment #comment-content ${selectorTimestampLink}`)
               .then(() => this.renderChaptersMarks(duration));
         },
         from_div(chaptersContainer = required()) {
            if (!(chaptersContainer instanceof HTMLElement)) return console.error('container not HTMLElement:', chaptersContainer);
            const
               progressContainerWidth = parseInt(getComputedStyle(chaptersContainer).width),
               chaptersOut = document.getElementById(`${SELECTOR_ID}-chapters`);
            for (const chapter of chaptersContainer.children) {
               const
                  newChapter = document.createElement('span'),
                  { width, marginLeft, marginRight } = getComputedStyle(chapter),
                  chapterMargin = parseInt(marginLeft) + parseInt(marginRight);
               newChapter.style.width = (((parseInt(width) + chapterMargin) / progressContainerWidth) * 100) + '%';
               chaptersOut.append(newChapter);
            }
         },
         renderChaptersMarks(duration) {
            if (isNaN(duration)) return console.error('duration isNaN:', duration);
            if (chaptersContainer = document.getElementById(`${SELECTOR_ID}-chapters`)) {
               chaptersContainer.innerHTML = '';
            }
            const chapterList = NOVA.getChapterList(duration);
            chapterList
               ?.forEach((chapter, i, chapters_list) => {
                  const newChapter = document.createElement('span');
                  const nextChapterSec = chapters_list[i + 1]?.sec || duration;
                  newChapter.style.width = ((nextChapterSec - chapter.sec) / duration) * 100 + '%';
                  if (chapter.title) newChapter.title = chapter.title;
                  newChapter.setAttribute('time', chapter.time);
                  chaptersContainer.append(newChapter);
               });
            return chapterList;
         },
      };
   },
});
window.nova_plugins.push({
   id: 'time-remaining',
   run_on_pages: 'watch, embed, -mobile',
   _runtime: user_settings => {
      const SELECTOR_ID = 'nova-player-time-remaining';
      NOVA.waitSelector('.ytp-time-duration, ytm-time-display .time-display-content')
         .then(container => {
            NOVA.waitSelector('video')
               .then(video => {
                  video.addEventListener('timeupdate', setRemaining.bind(video));
                  video.addEventListener('ratechange', setRemaining.bind(video));
                  ['suspend', 'ended'].forEach(evt => {
                     video.addEventListener(evt, () => insertToHTML({ 'container': container }));
                  });
                  document.addEventListener('yt-navigate-finish', () => insertToHTML({ 'container': container }));
               });
            function setRemaining() {
               if (isNaN(this.duration)
                  || movie_player.getVideoData().isLive
                  || (NOVA.currentPage == 'embed' && window.self.location.href.includes('live_stream'))
                  || document.visibilityState == 'hidden'
                  || movie_player.classList.contains('ytp-autohide')
               ) return;
               const
                  getProgressPt = () => {
                     const floatRound = pt => (this.duration > 3600) ? pt.toFixed(2)
                        : (this.duration > 1500) ? pt.toFixed(1)
                           : Math.round(pt);
                     return floatRound((this.currentTime / this.duration) * 100) + '%';
                  },
                  getLeftTime = () => '-' + NOVA.timeFormatTo.HMS.digit((this.duration - this.currentTime) / this.playbackRate);
               let text;
               switch (user_settings.time_remaining_mode) {
                  case 'pt': text = ' • ' + getProgressPt(); break;
                  case 'time': text = getLeftTime(); break;
                  default:
                     text = getLeftTime();
                     text += text && ` (${getProgressPt()})`;
               }
               if (text) {
                  insertToHTML({ 'text': text, 'container': container });
               }
            }
            function insertToHTML({ text = '', container = required() }) {
               if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
               (document.getElementById(SELECTOR_ID) || (function () {
                  container.insertAdjacentHTML('afterend', `&nbsp;<span id="${SELECTOR_ID}">${text}</span>`);
                  return document.getElementById(SELECTOR_ID);
               })())
                  .textContent = text;
            }
         });
   },
});
window.nova_plugins.push({
   id: 'related-visibility',
   run_on_pages: 'watch, -mobile',
   _runtime: user_settings => {
      NOVA.collapseElement({
         selector: '#secondary #related',
         remove: (user_settings.related_visibility_mode == 'disable') ? true : false,
      });
   },
});
window.nova_plugins.push({
   id: 'playlist-duration',
   run_on_pages: 'watch, playlist, -mobile',
   restart_on_location_change: true,
   _runtime: user_settings => {
      const
         SELECTOR_ID = 'nova-playlist-duration',
         playlistId = NOVA.queryURL.get('list');
      if (!playlistId) return;
      switch (NOVA.currentPage) {
         case 'playlist':
            NOVA.waitSelector('#owner-text a')
               .then(el => {
                  if (duration = getPlaylistDuration()) {
                     insertToHTML({ 'container': el, 'text': duration });
                  }
                  else {
                     getPlaylistDurationFromThumbnails({
                        'items_selector': '#primary .ytd-thumbnail-overlay-time-status-renderer:not(:empty)',
                     })
                        .then(duration => insertToHTML({ 'container': el, 'text': duration }));
                  }
                  function getPlaylistDuration() {
                     const vids_list = (document.body.querySelector('ytd-app')?.data?.response || window.ytInitialData)
                        .contents.twoColumnBrowseResultsRenderer
                        ?.tabs[0].tabRenderer?.content?.sectionListRenderer
                        ?.contents[0].itemSectionRenderer
                        ?.contents[0].playlistVideoListRenderer?.contents;
                     const duration = vids_list?.reduce((acc, vid) => acc + (isNaN(vid.playlistVideoRenderer?.lengthSeconds) ? 0 : parseInt(vid.playlistVideoRenderer.lengthSeconds)), 0);
                     if (duration) {
                        return outFormat(duration);
                     }
                  }
               });
            break;
         case 'watch':
            NOVA.waitSelector('#secondary .index-message-wrapper')
               .then(el => {
                  const waitPlaylist = setInterval(() => {
                     const
                        playlistLength = movie_player.getPlaylist()?.length,
                        playlistList = document.querySelector('yt-playlist-manager')?.currentPlaylistData_?.contents
                           .filter(e => e.playlistPanelVideoRenderer?.lengthText?.simpleText)
                           .map(e => NOVA.timeFormatTo.hmsToSec(e.playlistPanelVideoRenderer.lengthText.simpleText));
                     console.assert(playlistList?.length === playlistLength, 'playlist loading:', playlistList?.length + '/' + playlistLength);
                     if (playlistList?.length === playlistLength) {
                        clearInterval(waitPlaylist);
                        if (duration = getPlaylistDuration(playlistList)) {
                           insertToHTML({ 'container': el, 'text': duration });
                        }
                        else if (!user_settings.playlist_duration_progress_type) {
                           getPlaylistDurationFromThumbnails({
                              'container': document.body.querySelector('#secondary #playlist'),
                              'items_selector': '#playlist-items #unplayableText[hidden]',
                           })
                              .then(duration => insertToHTML({ 'container': el, 'text': duration }));
                        }
                     }
                  }, 1000);
                  function getPlaylistDuration(total_list) {
                     const currentIndex = movie_player.getPlaylistIndex();
                     let elapsedList = [...total_list];
                     switch (user_settings.playlist_duration_progress_type) {
                        case 'done':
                           elapsedList.splice(currentIndex);
                           break;
                        case 'left':
                           elapsedList.splice(0, currentIndex);
                           break;
                     }
                     const sumArr = arr => arr.reduce((acc, time) => acc + +time, 0);
                     return outFormat(
                        sumArr(elapsedList),
                        user_settings.playlist_duration_percentage ? sumArr(total_list) : false
                     );
                  }
               });
            break;
      }
      function getPlaylistDurationFromThumbnails({ items_selector = required(), container }) {
         if (container && !(container instanceof HTMLElement)) {
            return console.error('container not HTMLElement:', container);
         }
         return new Promise(resolve => {
            let forcePlaylistRun = false;
            const waitThumbnails = setInterval(() => {
               const
                  playlistLength = movie_player.getPlaylist()?.length
                     || document.body.querySelector('ytd-player')?.player_?.getPlaylist()?.length
                     || document.body.querySelectorAll(items_selector)?.length,
                  timeStampList = (container || document.body)
                     .querySelectorAll('.ytd-thumbnail-overlay-time-status-renderer:not(:empty)'),
                  duration = getTotalTime(timeStampList);
               console.assert(timeStampList.length === playlistLength, 'playlist loading:', timeStampList.length + '/' + playlistLength);
               if (+duration && timeStampList.length
                  && (timeStampList.length === playlistLength || forcePlaylistRun)
               ) {
                  clearInterval(waitThumbnails);
                  resolve(outFormat(duration));
               }
               else if (!forcePlaylistRun) {
                  setTimeout(() => forcePlaylistRun = true, 1000 * 3);
               }
            }, 500);
         });
         function getTotalTime(nodes) {
            const arr = [...nodes]
               .map(e => NOVA.timeFormatTo.hmsToSec(e.textContent))
               .filter(Number);
            return arr.length && arr.reduce((acc, time) => acc + +time, 0);
         }
      }
      function outFormat(duration = 0, total) {
         let outArr = [
            NOVA.timeFormatTo.HMS.digit(
               (NOVA.currentPage == 'watch' && NOVA.videoElement?.playbackRate)
                  ? (duration / NOVA.videoElement.playbackRate) : duration
            )
         ];
         if (total) {
            outArr.push(`(${~~(duration * 100 / total) + '%'})`);
            if (user_settings.playlist_duration_progress_type) {
               outArr.push(user_settings.playlist_duration_progress_type);
            }
         }
         return ' - ' + outArr.join(' ');
      }
      function insertToHTML({ text = '', container = required() }) {
         if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
         (container.querySelector(`#${SELECTOR_ID}`) || (function () {
            const el = document.createElement('span');
            el.id = SELECTOR_ID;
            return container.appendChild(el);
         })())
            .textContent = ' ' + text;
      }
   },
});
const Plugins = {
   run: ({ user_settings, app_ver }) => {
      if (!window.nova_plugins?.length) return console.error('nova_plugins empty', window.nova_plugins);
      if (!user_settings) return console.error('user_settings empty', user_settings);
      NOVA.currentPage = (function () {
         const [page, channelTab] = location.pathname.split('/').filter(Boolean);
         NOVA.channelTab = channelTab;
         return (page != 'live_chat')
            && (['channel', 'c', 'user'].includes(page)
               || page?.startsWith('@')
               || /[A-Z\d_]/.test(page)
               || ['featured', 'videos', 'shorts', 'streams', 'playlists', 'community', 'channels', 'about'].includes(channelTab)
            ) ? 'channel' : (page == 'clip') ? 'watch' : page || 'home';
      })();
      NOVA.isMobile = location.host == 'm.youtube.com';
      let logTableArray = [],
         logTableStatus,
         logTableTime;
      window.nova_plugins?.forEach(plugin => {
         const pagesAllowList = plugin?.run_on_pages?.split(',').map(p => p.trim().toLowerCase()).filter(Boolean);
         logTableTime = 0;
         logTableStatus = false;
         if (!pluginChecker(plugin)) {
            console.error('Plugin invalid\n', plugin);
            alert('Plugin invalid: ' + plugin?.id);
            logTableStatus = 'INVALID';
         }
         else if (plugin.was_init && !plugin.restart_on_location_change) {
            logTableStatus = 'skiped';
         }
         else if (!user_settings.hasOwnProperty(plugin.id)) {
            logTableStatus = 'off';
         }
         else if (
            (
               pagesAllowList?.includes(NOVA.currentPage)
               || (pagesAllowList?.includes('all') && !pagesAllowList?.includes('-' + NOVA.currentPage))
            )
            && (!NOVA.isMobile || (NOVA.isMobile && !pagesAllowList?.includes('-mobile')))
         ) {
            try {
               const startTableTime = performance.now();
               plugin.was_init = true;
               plugin._runtime(user_settings);
               logTableTime = (performance.now() - startTableTime).toFixed(2);
               logTableStatus = true;
            } catch (err) {
               console.groupEnd('plugins status');
               console.error(`[ERROR PLUGIN] ${plugin.id}\n${err.stack}\n\nPlease report the bug: https://github.com/raingart/Nova-YouTube-extension/issues/new?body=` + encodeURIComponent(app_ver + ' | ' + navigator.userAgent));
               if (user_settings.report_issues && _pluginsCaptureException) {
                  _pluginsCaptureException({
                     'trace_name': plugin.id,
                     'err_stack': err.stack,
                     'app_ver': app_ver,
                     'confirm_msg': `ERROR in Nova YouTube™\n\nCrash plugin: "${plugin.id}"\nPlease report the bug or disable the plugin\n\nSend the bug raport to developer?`,
                  });
               }
               console.groupCollapsed('plugins status');
               logTableStatus = 'ERROR';
            }
         }
         logTableArray.push({
            'launched': logTableStatus,
            'name': plugin?.id,
            'time init (ms)': logTableTime,
         });
      });
      console.table(logTableArray);
      console.groupEnd('plugins status');
      function pluginChecker(plugin) {
         const result = plugin?.id && plugin.run_on_pages && 'function' === typeof plugin._runtime;
         if (!result) {
            console.error('plugin invalid:\n', {
               'id': plugin?.id,
               'run_on_pages': plugin?.run_on_pages,
               '_runtime': 'function' === typeof plugin?._runtime,
            });
         }
         return result;
      }
   },
}
console.log('%c ', 'color:#0096fa; font-weight:bold;', GM_info.script.name + ' v.' + GM_info.script.version);
const
   configPage = 'https://raingart.github.io/options.html',
   configStoreName = 'user_settings',
   user_settings = GM_getValue(configStoreName, null);
if (user_settings?.exclude_iframe && (window.frameElement || window.self !== window.top)) {
   return console.warn(GM_info.script.name + ': processed in the iframe disable');
}
console.debug(`current ${configStoreName}:`, user_settings);
const keyRenameTemplate = {
   'shorts_thumbnails_time': 'shorts-thumbnails-time',
}
for (const oldKey in user_settings) {
   if (newKey = keyRenameTemplate[oldKey]) {
      console.log(oldKey, '=>', newKey);
      delete Object.assign(user_settings, { [newKey]: user_settings[oldKey] })[oldKey];
   }
   GM_setValue(configStoreName, user_settings);
}
registerMenuCommand();
if (location.hostname === new URL(configPage).hostname) setupConfigPage();
else {
   if (!user_settings?.disable_setting_button) insertSettingButton();
   if (!user_settings || !Object.keys(user_settings).length) {
      if (confirm('Active plugins undetected. Open the settings page now?')) GM_openInTab(configPage);
      user_settings['report_issues'] = 'on';
      GM_setValue(configStoreName, user_settings);
   }
   else landerPlugins();
}
function setupConfigPage() {
   document.addEventListener('submit', event => {
      event.preventDefault();
      let obj = {};
      for (const [key, value] of new FormData(event.target)) {
         if (obj.hasOwnProperty(key)) {
            obj[key] += ',' + value;
            obj[key] = obj[key].split(',');
         }
         else {
            switch (value) {
               case 'true': obj[key] = true; break;
               case 'false': obj[key] = false; break;
               case 'undefined': obj[key] = undefined; break;
               default: obj[key] = value;
            }
         };
      }
      console.debug(`update ${configStoreName}:`, obj);
      GM_setValue(configStoreName, obj);
   });
   window.addEventListener('DOMContentLoaded', () => {
      localizePage(user_settings?.lang_code);
      storeData = user_settings;
   });
   window.addEventListener('load', () => {
      document.body?.classList?.remove('preload');
      document.body.querySelector('a[href$="issues/new"]')
         .addEventListener('click', ({ target }) => {
            target.href += '?body=' + encodeURIComponent(GM_info.script.version + ' | ' + navigator.userAgent);
         });
   });
}
function landerPlugins() {
   processLander();
   function processLander() {
      const plugins_lander = setInterval(() => {
         const domLoaded = document?.readyState != 'loading';
         if (!domLoaded) return console.debug('waiting, page loading..');
         clearInterval(plugins_lander);
         console.groupCollapsed('plugins status');
         Plugins.run({
            'user_settings': user_settings,
            'app_ver': GM_info.script.version,
         });
      }, 500);
   }
   let prevURL = location.href;
   const isURLChanged = () => prevURL == location.href ? false : prevURL = location.href;
   if (isMobile = (location.host == 'm.youtube.com')) {
      window.addEventListener('transitionend', ({ target }) => target.id == 'progress' && isURLChange() && processLander());
   }
   else {
      document.addEventListener('yt-navigate-start', () => isURLChanged() && processLander());
   }
}
function registerMenuCommand() {
   GM_registerMenuCommand('Settings', () => GM_openInTab(configPage));
   GM_registerMenuCommand('Import settings', () => {
      const f = document.createElement('input');
      f.type = 'file';
      f.accept = 'application/JSON';
      f.style.display = 'none';
      f.addEventListener('change', function () {
         if (f.files.length !== 1) return alert('file empty');
         const rdr = new FileReader();
         rdr.addEventListener('load', function () {
            try {
               GM_setValue(configStoreName, JSON.parse(rdr.result));
               alert('Settings imported');
               location.reload();
            }
            catch (err) {
               alert(`Error parsing settings\n${err.name}: ${err.message}`);
            }
         });
         rdr.addEventListener('error', error => alert('Error loading file\n' + rdr?.error || error));
         rdr.readAsText(f.files[0]);
      });
      document.body.append(f);
      f.click();
      f.remove();
   });
   GM_registerMenuCommand('Export settings', () => {
      let d = document.createElement('a');
      d.style.display = 'none';
      d.download = 'nova-settings.json';
      d.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(user_settings));
      document.body.append(d);
      d.click();
      d.remove();
   });
}
function insertSettingButton() {
   NOVA.waitSelector('#masthead #end')
      .then(menu => {
         const
            titleMsg = 'Nova Settings',
            a = document.createElement('a'),
            SETTING_BTN_ID = 'nova_settings_button';
         a.id = SETTING_BTN_ID;
         a.href = configPage + '?tabs=tab-plugins';
         a.target = '_blank';
         a.innerHTML =
            `<yt-icon-button class="style-scope ytd-button-renderer style-default size-default">
               <svg viewBox="-4 0 20 16">
                  <radialGradient id="nova-gradient" gradientUnits="userSpaceOnUse" cx="6" cy="22" r="18.5">
                     <stop class="nova-gradient-start" offset="0"/>
                     <stop class="nova-gradient-stop" offset="1"/>
                  </radialGradient>
                  <g fill="deepskyblue">
                     <polygon points="0,16 14,8 0,0"/>
                  </g>
               </svg>
            </yt-icon-button>`;
         a.addEventListener('click', () => {
            setTimeout(() => document.body.click(), 200);
         });
         a.title = titleMsg;
         const tooltip = document.createElement('tp-yt-paper-tooltip');
         tooltip.className = 'style-scope ytd-topbar-menu-button-renderer';
         tooltip.textContent = titleMsg;
         a.appendChild(tooltip);
         menu.prepend(a);
         NOVA.css.push(
            `#${SETTING_BTN_ID}[tooltip]:hover:after {
               position: absolute;
               top: 50px;
               transform: translateX(-50%);
               content: attr(tooltip);
               text-align: center;
               min-width: 3em;
               max-width: 21em;
               white-space: nowrap;
               overflow: hidden;
               text-overflow: ellipsis;
               padding: 1.8ch 1.2ch;
               border-radius: 0.6ch;
               background-color: #616161;
               box-shadow: 0 1em 2em -0.5em rgb(0 0 0 / 35%);
               color: #fff;
               z-index: 1000;
            }
            #${SETTING_BTN_ID} {
               position: relative;
               opacity: .3;
               transition: opacity .3s ease-out;
            }
            #${SETTING_BTN_ID}:hover {
               opacity: 1 !important;
            }
            #${SETTING_BTN_ID} path,
            #${SETTING_BTN_ID} polygon {
               fill: url(#nova-gradient);
            }
            #${SETTING_BTN_ID} .nova-gradient-start,
            #${SETTING_BTN_ID} .nova-gradient-stop {
               transition: .6s;
               stop-color: #7a7cbd;
            }
            #${SETTING_BTN_ID}:hover .nova-gradient-start {
               stop-color: #0ff;
            }
            #${SETTING_BTN_ID}:hover .nova-gradient-stop {
               stop-color: #0095ff;
            }`);
      });
}
function _pluginsCaptureException({ trace_name, err_stack, confirm_msg, app_ver }) {
}