Greasy Fork

Greasy Fork is available in English.

YouTube: Audio Only

No Video Streaming

当前为 2024-01-15 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name                YouTube: Audio Only
// @description         No Video Streaming
// @namespace           UserScript
// @version             0.4.0
// @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;

    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;
          }
        } 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;
    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;
        //   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 px = 0;
      let fa = 0;
      document.addEventListener('durationchange', (evt) => {

        if (evt.target.readyState !== 1) {
          fa = 1;
          if (px) clearTimeout(px);
          px = setTimeout_(() => {

            let qq = 0;
            let cid = setInterval(() => {
              let q = document.querySelector('#movie_player');
              if (!q) return;
              let a = document.querySelector('.video-stream.html5-main-video');
              if (a.muted) return;

              if (qq) return;
              qq = 1;
              clearInterval(cid);

              if (px) clearTimeout(px);
              px = setTimeout_(() => {


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

                if (fa !== 1) return;
                document.querySelector('#movie_player').click();

              }, 400)

            }, 400)


          }, 400);
          return;
        } 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);
  })


})();