YouTube: Audio Only

No Video Streaming

目前為 2024-01-15 提交的版本,檢視 最新版本

// ==UserScript==
// @name                YouTube: Audio Only
// @description         No Video Streaming
// @namespace           UserScript
// @version             0.4.1
// @author              CY Fung
// @match               https://www.youtube.com/*
// @match               https://www.youtube.com/embed/*
// @match               https://www.youtube-nocookie.com/embed/*
// @match               https://m.youtube.com/*
// @exclude             /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
// @icon                https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/YouTube-Audio-Only.png
// @grant               GM_registerMenuCommand
// @grant               GM.setValue
// @grant               GM.getValue
// @run-at              document-start
// @license             MIT
// @compatible          chrome
// @compatible          firefox
// @compatible          opera
// @compatible          edge
// @compatible          safari
// @allFrames           true
//
// ==/UserScript==

(async function () {
  'use strict';

  let setTimeout_ = setTimeout;

  /** @type {globalThis.PromiseConstructor} */
  const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.

  async function confirm(message) {
    // Create the HTML for the dialog

    if (!document.body) return;

    let dialog = document.getElementById('confirmDialog794');
    if (!dialog) {

      const dialogHTML = `
          <div id="confirmDialog794" class="dialog-style" style="display: block;">
              <div class="confirm-box">
                  <p>${message}</p>
                  <div class="confirm-buttons">
                      <button id="confirmBtn">Confirm</button>
                      <button id="cancelBtn">Cancel</button>
                  </div>
              </div>
          </div>
      `;

      // Append the dialog to the document body
      document.body.insertAdjacentHTML('beforeend', dialogHTML);
      dialog = document.getElementById('confirmDialog794');

    }

    // Return a promise that resolves or rejects based on the user's choice
    return new Promise((resolve) => {
      document.getElementById('confirmBtn').onclick = () => {
        resolve(true);
        cleanup();
      };

      document.getElementById('cancelBtn').onclick = () => {
        resolve(false);
        cleanup();
      };

      function cleanup() {
        dialog && dialog.remove();
        dialog = null;
      }
    });
  }



  if (location.pathname === '/live_chat' || location.pathname === 'live_chat_replay') return;


  const pageInjectionCode = function () {


    /** @type {globalThis.PromiseConstructor} */
    const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.

    const PromiseExternal = ((resolve_, reject_) => {
      const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
      return class PromiseExternal extends Promise {
        constructor(cb = h) {
          super(cb);
          if (cb === h) {
            /** @type {(value: any) => void} */
            this.resolve = resolve_;
            /** @type {(reason?: any) => void} */
            this.reject = reject_;
          }
        }
      };
    })();



    const observablePromise = (proc, timeoutPromise) => {
      let promise = null;
      return {
        obtain() {
          if (!promise) {
            promise = new Promise(resolve => {
              let mo = null;
              const f = () => {
                let t = proc();
                if (t) {
                  mo.disconnect();
                  mo.takeRecords();
                  mo = null;
                  resolve(t);
                }
              }
              mo = new MutationObserver(f);
              mo.observe(document, { subtree: true, childList: true })
              f();
              timeoutPromise && timeoutPromise.then(() => {
                resolve(null)
              });
            });
          }
          return promise
        }
      }
    }


    let vcc = 0;
    let vdd = -1;

    let u33 = null;

    let cv = null;
    document.addEventListener('durationchange', (evt) => {
      const target = (evt || 0).target;
      if (!(target instanceof HTMLMediaElement)) return;

      if (target.classList.contains('video-stream') && target.classList.contains('html5-main-video')) {

        if (target.readyState === 1) {

          vcc++;

        }
        if (target.readyState === 1 && target.networkState === 2) {
          target.__spfgs__ = true;
          if (u33) {
            u33.resolve();
            u33 = null;
          }
          if (cv) {
            cv.resolve();
            cv = null;
          }
        } else {
          target.__spfgs__ = false;

        }

      }
    }, true);



    // XMLHttpRequest.prototype.open299 = XMLHttpRequest.prototype.open;
    /*

    XMLHttpRequest.prototype.open2 = function(method, url, ...args){

          if (typeof url === 'string' && url.length > 24 && url.includes('/videoplayback?') && url.replace('?', '&').includes('&source=')) {
            if (vcc !== vdd) {
              vdd = vcc;
              window.postMessage({ ZECxh: url.includes('source=yt_live_broadcast') }, "*");
            }
          }

      return this.open299(method, url, ...args)
    }*/



    // desktop only
    // document.addEventListener('yt-page-data-fetched', async (evt) => {

    //   const pageFetchedDataLocal = evt.detail;
    //   let isLiveNow;
    //   try {
    //     isLiveNow = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.isLiveNow;
    //   } catch (e) { }
    //   window.postMessage({ ZECxh: isLiveNow === true }, "*");

    // }, false);

    // return;

    // let clickLockFn = null;

    let clickLockFn = null;
    let clickTarget = null;
    if (location.origin === 'https://m.youtube.com') {



      EventTarget.prototype.addEventListener322 = EventTarget.prototype.addEventListener;

      EventTarget.prototype.addEventListener = function (evt, fn, opts) {

        if (evt === 'visibilitychange') {
          evt += 'y'
        }
        let hn = fn;

        if (evt === 'click' && this.id === 'movie_player') {


          clickLockFn = fn; clickTarget = this;
          //   hn = function (e) {

          //     // console.log(22 ,  e)
          //     // console.log(433, e.type, e.detail, fn);
          //     // window.em33 =  true;
          //     //             if(e && e.type !=='updateui' && e.type!=='success' && e.type!==''){
          //     //             console.log(433, e.type, e.detail);

          //     //             }
          //     return fn.apply(this, arguments)
          //   }

        }

        /*

        if(evt ==='player-state-change' || evt == "player-autonav-pause" || evt === "video-data-change" || evt === "state-navigatestart"){

          hn = function(){

            let e = arguments[0];
            if(e){
            console.log(213, e.type, e.detail);

            }
            return fn.apply(this,  arguments)
          }
        }
        */

        return this.addEventListener322(evt, hn, opts)

      }

      /*
      const XMLHttpRequest_ = XMLHttpRequest;

      (() => {
        XMLHttpRequest = class XMLHttpRequest extends XMLHttpRequest_ {
          constructor(...args) {
            super(...args);
          }
          open(method, url, ...args) {

            if (typeof url === 'string' && url.length > 24 && url.includes('/videoplayback?') && url.replace('?', '&').includes('&source=')) {
              if (vcc !== vdd) {
                vdd = vcc;
                window.postMessage({ ZECxh: url.includes('source=yt_live_broadcast') }, "*");
              }
            }
            return super.open(method, url, ...args)
          }
        }
      })();
      */
    }


    let setTimeout_ = setTimeout;


    if (location.origin === 'https://www.youtube.com') {


      document.addEventListener('yt-navigate-finish', async () => {

        const fn = () => {

          const elm = document.querySelector('ytd-player#ytd-player');
          if (!elm) return;
          const cnt = elm.polymerController || elm.inst || elm;
          if (!cnt) return;

          if (!cnt.player_) return;
          if (!cnt.player_.playVideo) return;

          return { elm, cnt };
        }
        let o = fn();
        if (!o) {
          o = await observablePromise(fn).obtain()
        }
        const { cnt, elm } = o;
        if (!cnt || !cnt.player_ || !cnt.player_.playVideo) return;
        if (cnt.player_.getPlayerState() === 3) {
          const audio = HTMLElement.prototype.querySelector.call(elm, '.video-stream.html5-main-video');
          if (audio.__spfgs__ !== true) { // undefined or false
            u33 = new PromiseExternal();
            await u33.then();
          }

          if (cnt.player_.getPlayerState() !== 3 || !audio.isConnected) return;
          if (audio && audio.__spfgs__ === true) {
            await cnt.player_.cancelPlayback();

            await new Promise(resolve => window.setTimeout(resolve, 1));
            await cnt.player_.playVideo();

          }
        }

      });

    } else if (location.origin === 'https://m.youtube.com') {


      let qm = new PromiseExternal();


      //       document.addEventListener('DOMContentLoaded', (evt) => {

      //         const mo = new MutationObserver((mutations)=>{

      //           console.log(5899, mutations)

      //         });
      //         mo.observe(document, {subtree: true, childList: true})


      //       })



      //       window.addEventListener('onReady', (evt) => {

      //         console.log(6811)
      //       }, true);

      //       window.addEventListener('localmediachange', (evt) => {

      //         console.log(6812)
      //       }, true);

      //       window.addEventListener('onVideoDataChange', (evt) => {

      //         console.log(6813)
      //       }, true);

      window.addEventListener('state-navigateend', async (evt) => {

        // console.log(5910)
        if (clickLockFn && clickTarget) {


          let a = HTMLElement.prototype.querySelector.call(clickTarget, '.video-stream.html5-main-video');

          if (a) {

            if (a.muted === true && a.paused === true) clickLockFn.call(clickTarget, { type: 'click', target: clickTarget })
            // console.log(588, clickLockFn, a.muted)

            if (a.__spfgs__ !== true && a.paused === true) {
              clickLockFn.call(clickTarget, { type: 'click', target: clickTarget })
            }



          }

        }
        qm.resolve();


      }, true);

      //       window.addEventListener('onStateChange', (evt) => {

      //         console.log(6815)
      //       }, true);

      let px = 0;
      let fa = 0;
      let ez = 0;

      async function delayRun() {



        await qm.then();
        if (ez) return;
        ez = 1;


        let qq = 0;

        const { q, a } = await observablePromise(() => {

          let q = document.querySelector('#movie_player');
          if (!q) return;
          let a = document.querySelector('.video-stream.html5-main-video');
          if (!a) return;
          return { q, a };

        }).obtain();

        if (a.muted) return;

        if (a.muted === false && a.readyState === 0 && a.networkState === 2) {

        } else {
          return;
        }

        let cid = setInterval(() => {
          if (a.muted === false && a.readyState === 0 && a.networkState === 2) {

          } else {
            clearInterval(cid);
          }
          if (a.paused !== true) return;
          clearInterval(cid);

          if (qq) return;
          qq = 1;

          if (document.querySelector('.player-controls-content')) return;

          if (fa !== 1) return;

          if (a.paused === true && a.muted === false && a.readyState === 0 && a.networkState === 2) document.querySelector('#movie_player').click();
          // console.log(a.paused)
          // console.log(7710)



        }, 10)



      }

      document.addEventListener('durationchange', (evt) => {

        // console.log(5911)


        if (evt.target.readyState !== 1) {
          fa = 1;
          if (px) clearTimeout(px);
          px = setTimeout_(delayRun, 100);
        } else {
          fa = 2;
        }
        // console.log(123123, evt.target, evt.target.duration)


      }, true)



    }



    let prepared = false;
    function prepare() {
      if (prepared) return;
      prepared = true;

      if (typeof _yt_player !== 'undefined' && _yt_player && typeof _yt_player === 'object') {

        for (const [k, v] of Object.entries(_yt_player)) {

          if (typeof v === 'function' && typeof v.prototype.clone === 'function'
            && typeof v.prototype.get === 'function' && typeof v.prototype.set === 'function'

            && typeof v.prototype.isEmpty === 'undefined' && typeof v.prototype.forEach === 'undefined'
            && typeof v.prototype.clear === 'undefined'

          ) {

            key = k;

          }

        }

      }

      if (key) {

        const ClassX = _yt_player[key];
        _yt_player[key] = class extends ClassX {
          constructor(...args) {

            if (typeof args[0] === 'string' && args[0].startsWith('http://')) args[0] = '';
            super(...args);

          }
        }
        _yt_player[key].luX1Y = 1;
      }

    }
    let s3 = Symbol();
    Object.defineProperty(Object.prototype, 'deviceIsAudioOnly', {
      get() {
        return this[s3];
      },
      set(nv) {
        if ('ATTRIBUTE_NODE' in this) {

        } else {
          if (typeof nv === 'boolean') this[s3] = true;
          else this[s3] = undefined;
          prepare();
        }
        return true;
      },
      enumerable: false,
      configurable: true
    });


    let s1 = Symbol();
    let s2 = Symbol();
    Object.defineProperty(Object.prototype, 'defraggedFromSubfragments', {
      get() {
        return undefined;
      },
      set(nv) {
        return true;
      },
      enumerable: false,
      configurable: true
    });

    Object.defineProperty(Object.prototype, 'hasSubfragmentedFmp4', {
      get() {
        return this[s1];
      },
      set(nv) {
        if (typeof nv === 'boolean') this[s1] = false;
        else this[s1] = undefined;
        return true;
      },
      enumerable: false,
      configurable: true
    });

    Object.defineProperty(Object.prototype, 'hasSubfragmentedWebm', {
      get() {
        return this[s2];
      },
      set(nv) {
        if (typeof nv === 'boolean') this[s2] = false;
        else this[s2] = undefined;
        return true;
      },
      enumerable: false,
      configurable: true
    });


    const supportedFormatsConfig = () => {

      function typeTest(type) {
        if (typeof type === 'string' && type.startsWith('video/')) {
          return false;
        }
      }

      // return a custom MIME type checker that can defer to the original function
      function makeModifiedTypeChecker(origChecker) {
        // Check if a video type is allowed
        return function (type) {
          let res = undefined;
          if (type === undefined) res = false;
          else {
            res = typeTest.call(this, type);
          }
          if (res === undefined) res = origChecker.apply(this, arguments);
          return res;
        };
      }

      // Override video element canPlayType() function
      const proto = (HTMLVideoElement || 0).prototype;
      if (proto && typeof proto.canPlayType == 'function') {
        proto.canPlayType = makeModifiedTypeChecker(proto.canPlayType);
      }

      // Override media source extension isTypeSupported() function
      const mse = window.MediaSource;
      // Check for MSE support before use
      if (mse && typeof mse.isTypeSupported == 'function') {
        mse.isTypeSupported = makeModifiedTypeChecker(mse.isTypeSupported);
      }

    };

    supportedFormatsConfig();
  }

  const isEnable = (typeof GM !== 'undefined' && typeof GM.getValue === 'function') ? (await GM.getValue("isEnable_aWsjF", true)) : null;
  if (typeof isEnable !== 'boolean') throw new DOMException("Please Update your browser", "NotSupportedError");
  if (isEnable) {
    const element = document.createElement('button');
    element.setAttribute('onclick', `(${pageInjectionCode})()`);
    element.click();
  }

  GM_registerMenuCommand(`Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`, async function () {
    await GM.setValue("isEnable_aWsjF", !isEnable);
    location.reload();
  });

  let messageCount = 0;
  let busy = false;
  window.addEventListener('message', (evt) => {

    const v = ((evt || 0).data || 0).ZECxh;
    if (typeof v === 'boolean') {
      if (messageCount > 1e9) messageCount = 9;
      const t = ++messageCount;
      if (v && isEnable) {
        requestAnimationFrame(async () => {
          if (t !== messageCount) return;
          if (busy) return;
          busy = true;
          if (await confirm("Livestream is detected. Press OK to disable YouTube Audio Mode.")) {
            await GM.setValue("isEnable_aWsjF", !isEnable);
            location.reload();
          }
          busy = false;
        });
      }
    }

  });


  const pLoad = new Promise(resolve => {
    if (document.readyState !== 'loading') {
      resolve();
    } else {
      window.addEventListener("DOMContentLoaded", resolve, false);
    }
  });


  function contextmenuInfoItemAppearedFn(target) {

    const btn = target.closest('.ytp-menuitem[role="menuitem"]');
    if (!btn) return;
    if (btn.parentNode.querySelector('.ytp-menuitem[role="menuitem"].audio-only-toggle-btn')) return;
    document.documentElement.classList.add('with-audio-only-toggle-btn');
    const newBtn = btn.cloneNode(true)
    newBtn.querySelector('.ytp-menuitem-label').textContent = `Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`;
    newBtn.classList.add('audio-only-toggle-btn');
    btn.parentNode.insertBefore(newBtn, btn.nextSibling);
    newBtn.addEventListener('click', async () => {
      await GM.setValue("isEnable_aWsjF", !isEnable);
      location.reload();
    });
  }


  function mobileMenuItemAppearedFn(target) {

    const btn = target.closest('ytm-menu-item');
    if (!btn) return;
    if (btn.parentNode.querySelector('ytm-menu-item.audio-only-toggle-btn')) return;
    document.documentElement.classList.add('with-audio-only-toggle-btn');
    const newBtn = btn.cloneNode(true);
    newBtn.querySelector('.menu-item-button').textContent = `Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`;
    newBtn.classList.add('audio-only-toggle-btn');
    btn.parentNode.insertBefore(newBtn, btn.nextSibling);
    newBtn.addEventListener('click', async () => {
      await GM.setValue("isEnable_aWsjF", !isEnable);
      location.reload();
    });
  }




  pLoad.then(() => {

    document.addEventListener('animationstart', (evt) => {
      const animationName = evt.animationName;
      if (!animationName) return;

      if (animationName === 'contextmenuInfoItemAppeared') contextmenuInfoItemAppearedFn(evt.target);
      if (animationName === 'mobileMenuItemAppeared') mobileMenuItemAppearedFn(evt.target);

    }, true);


    const style = document.createElement('style');
    style.textContent = `
       @keyframes mobileMenuItemAppeared {
           0% {
               background-position-x: 3px;
          }
           100% {
               background-position-x: 4px;
          }
      }
       ytm-select.player-speed-settings ~ ytm-menu-item:last-of-type {
           animation: mobileMenuItemAppeared 1ms linear 0s 1 normal forwards;
      }
       @keyframes contextmenuInfoItemAppeared {
           0% {
               background-position-x: 3px;
          }
           100% {
               background-position-x: 4px;
          }
      }
       .ytp-contextmenu .ytp-menuitem[role="menuitem"] path[d^="M22 34h4V22h-4v12zm2-30C12.95"]{
           animation: contextmenuInfoItemAppeared 1ms linear 0s 1 normal forwards;
      }
       .with-audio-only-toggle-btn .ytp-contextmenu, .ytp-panel-menu, .ytp-panel {
           height: 40vh !important;
      }
       #confirmDialog794 {
       z-index:999999 !important;
           display: none;
          /* Hidden by default */
           position: fixed;
          /* Stay in place */
           z-index: 1;
          /* Sit on top */
           left: 0;
           top: 0;
           width: 100%;
          /* Full width */
           height: 100%;
          /* Full height */
           overflow: auto;
          /* Enable scroll if needed */
           background-color: rgba(0,0,0,0.4);
          /* Black w/ opacity */
      }
       #confirmDialog794 .confirm-box {
       position:relative;
       color: black;

       z-index:999999 !important;
           background-color: #fefefe;
           margin: 15% auto;
          /* 15% from the top and centered */
           padding: 20px;
           border: 1px solid #888;
           width: 30%;
          /* Could be more or less, depending on screen size */
           box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
      }
       #confirmDialog794 .confirm-buttons {
           text-align: right;
      }
       #confirmDialog794 button {
           margin-left: 10px;
      }



    `
    document.head.appendChild(style);
  })


})();