YouTube: Audio Only

No Video Streaming

目前为 2024-01-14 提交的版本,查看 最新版本

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

  /** @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 () {

    let vcc = 0;
    let vdd = -1;

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

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

        vcc++;

      }
    }, true)

    // embed & desktop & mobile
    window.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);
        }
      }

      return XMLHttpRequest;

    })(window.XMLHttpRequest);

    // 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);

    Object.defineProperty(Object.prototype, 'deviceIsAudioOnly', {
      get() {
        return true;
      },
      set(nv) {
        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 {
           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 {
           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);
  })


})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址