3D Youtube Downloader Helper

One click to send YouTube video url to 3D YouTube Downloader.

目前为 2019-10-02 提交的版本。查看 最新版本

// ==UserScript==
// @name        3D Youtube Downloader Helper
// @namespace   https://riophae.com/
// @version     0.1.3
// @description One click to send YouTube video url to 3D YouTube Downloader.
// @author      Riophae Lee
// @match       https://www.youtube.com/*
// @run-at      document-start
// @grant       none
// ==/UserScript==

(function () {
    'use strict';

    // Types inspired by
    // https://github.com/Microsoft/TypeScript/blob/9d3707d/src/lib/dom.generated.d.ts#L10581
    // Type predicate for TypeScript
    function isQueryable(object) {
        return typeof object.querySelectorAll === 'function';
    }
    function select(selectors, baseElement) {
        // Shortcut with specified-but-null baseElement
        if (arguments.length === 2 && !baseElement) {
            return null;
        }
        return (baseElement || document).querySelector(selectors);
    }
    function selectLast(selectors, baseElement) {
        // Shortcut with specified-but-null baseElement
        if (arguments.length === 2 && !baseElement) {
            return null;
        }
        const all = (baseElement || document).querySelectorAll(selectors);
        return all[all.length - 1];
    }
    /**
     * @param selectors      One or more CSS selectors separated by commas
     * @param [baseElement]  The element to look inside of
     * @return               Whether it's been found
     */
    function selectExists(selectors, baseElement) {
        if (arguments.length === 2) {
            return Boolean(select(selectors, baseElement));
        }
        return Boolean(select(selectors));
    }
    function selectAll(selectors, baseElements) {
        // Shortcut with specified-but-null baseElements
        if (arguments.length === 2 && !baseElements) {
            return [];
        }
        // Can be: select.all('selectors') or select.all('selectors', singleElementOrDocument)
        if (!baseElements || isQueryable(baseElements)) {
            const elements = (baseElements || document).querySelectorAll(selectors);
            return Array.apply(null, elements);
        }
        const all = [];
        for (let i = 0; i < baseElements.length; i++) {
            const current = baseElements[i].querySelectorAll(selectors);
            for (let ii = 0; ii < current.length; ii++) {
                all.push(current[ii]);
            }
        }
        // Preserves IE11 support and performs 3x better than `...all` in Safari
        const arr = [];
        all.forEach(function (v) {
            arr.push(v);
        });
        return arr;
    }
    select.last = selectLast;
    select.exists = selectExists;
    select.all = selectAll;
    var selectDom = select;

    var global$1 = (typeof global !== "undefined" ? global :
                typeof self !== "undefined" ? self :
                typeof window !== "undefined" ? window : {});

    // from https://github.com/kumavis/browser-process-hrtime/blob/master/index.js
    var performance = global$1.performance || {};
    var performanceNow =
      performance.now        ||
      performance.mozNow     ||
      performance.msNow      ||
      performance.oNow       ||
      performance.webkitNow  ||
      function(){ return (new Date()).getTime() };

    function createCommonjsModule(fn, module) {
    	return module = { exports: {} }, fn(module, module.exports), module.exports;
    }

    var manyKeysMap = createCommonjsModule(function (module) {

    const getInternalKeys = Symbol('getInternalKeys');
    const getPrivateKey = Symbol('getPrivateKey');
    const publicKeys = Symbol('publicKeys');
    const objectHashes = Symbol('objectHashes');
    const symbolHashes = Symbol('symbolHashes');
    const nullKey = Symbol('null'); // `objectHashes` key for null

    let keyCounter = 0;
    function checkKeys(keys) {
    	if (!Array.isArray(keys)) {
    		throw new TypeError('The keys parameter must be an array');
    	}
    }

    module.exports = class ManyKeysMap extends Map {
    	constructor() {
    		super();

    		this[objectHashes] = new WeakMap();
    		this[symbolHashes] = new Map(); // https://github.com/tc39/ecma262/issues/1194
    		this[publicKeys] = new Map();

    		// eslint-disable-next-line prefer-rest-params
    		const [pairs] = arguments; // Map compat
    		if (pairs === null || pairs === undefined) {
    			return;
    		}

    		if (typeof pairs[Symbol.iterator] !== 'function') {
    			throw new TypeError(typeof pairs + ' is not iterable (cannot read property Symbol(Symbol.iterator))');
    		}

    		for (const [keys, value] of pairs) {
    			this.set(keys, value);
    		}
    	}

    	[getInternalKeys](keys, create = false) {
    		const privateKey = this[getPrivateKey](keys, create);

    		let publicKey;
    		if (privateKey && this[publicKeys].has(privateKey)) {
    			publicKey = this[publicKeys].get(privateKey);
    		} else if (create) {
    			publicKey = [...keys]; // Regenerate keys array to avoid external interaction
    			this[publicKeys].set(privateKey, publicKey);
    		}

    		return {privateKey, publicKey};
    	}

    	[getPrivateKey](keys, create = false) {
    		const privateKeys = [];
    		for (let key of keys) {
    			if (key === null) {
    				key = nullKey;
    			}

    			const hashes = typeof key === 'object' || typeof key === 'function' ? objectHashes : typeof key === 'symbol' ? symbolHashes : false;

    			if (!hashes) {
    				privateKeys.push(key);
    			} else if (this[hashes].has(key)) {
    				privateKeys.push(this[hashes].get(key));
    			} else if (create) {
    				const privateKey = `@@mkm-ref-${keyCounter++}@@`;
    				this[hashes].set(key, privateKey);
    				privateKeys.push(privateKey);
    			} else {
    				return false;
    			}
    		}

    		return JSON.stringify(privateKeys);
    	}

    	set(keys, value) {
    		checkKeys(keys);
    		const {publicKey} = this[getInternalKeys](keys, true);
    		return super.set(publicKey, value);
    	}

    	get(keys) {
    		checkKeys(keys);
    		const {publicKey} = this[getInternalKeys](keys);
    		return super.get(publicKey);
    	}

    	has(keys) {
    		checkKeys(keys);
    		const {publicKey} = this[getInternalKeys](keys);
    		return super.has(publicKey);
    	}

    	delete(keys) {
    		checkKeys(keys);
    		const {publicKey, privateKey} = this[getInternalKeys](keys);
    		return Boolean(publicKey && super.delete(publicKey) && this[publicKeys].delete(privateKey));
    	}

    	clear() {
    		super.clear();
    		this[symbolHashes].clear();
    		this[publicKeys].clear();
    	}

    	get [Symbol.toStringTag]() {
    		return 'ManyKeysMap';
    	}

    	get size() {
    		return super.size;
    	}
    };
    });

    const pDefer = () => {
    	const deferred = {};

    	deferred.promise = new Promise((resolve, reject) => {
    		deferred.resolve = resolve;
    		deferred.reject = reject;
    	});

    	return deferred;
    };

    var pDefer_1 = pDefer;

    const cache = new manyKeysMap();
    const isDomReady = () => document.readyState === 'interactive' || document.readyState === 'complete';

    const elementReady = (selector, {
    	target = document,
    	stopOnDomReady = true,
    	timeout = Infinity
    } = {}) => {
    	const cacheKeys = [target, selector, stopOnDomReady, timeout];
    	const cachedPromise = cache.get(cacheKeys);
    	if (cachedPromise) {
    		return cachedPromise;
    	}

    	let rafId;
    	const deferred = pDefer_1();
    	const {promise} = deferred;

    	cache.set(cacheKeys, promise);

    	const stop = () => {
    		cancelAnimationFrame(rafId);
    		cache.delete(cacheKeys, promise);
    		deferred.resolve();
    	};

    	if (timeout !== Infinity) {
    		setTimeout(stop, timeout);
    	}

    	// Interval to keep checking for it to come into the DOM.
    	(function check() {
    		const element = target.querySelector(selector);

    		if (element) {
    			deferred.resolve(element);
    			stop();
    		} else if (stopOnDomReady && isDomReady()) {
    			stop();
    		} else {
    			rafId = requestAnimationFrame(check);
    		}
    	})();

    	return Object.assign(promise, {stop});
    };

    var elementReady_1 = elementReady;

    

    const FALLBACK_LANG = 'en-US';
    const ID_SUFFIX = '3d-youtube-downloader-helper';

    let isMenuOpen = false;
    let isTooltipShown = false;
    let justOpenedMenu = false;

    function memoize(fn) {
      let value;

      return () => {
        if (fn) {
          value = fn();

          if (value != null) {
            fn = null;
          }
        }

        return value
      }
    }

    const isWindowsOS = () => navigator.platform === 'Win32';
    const isEmbeddedVideo = () => window.location.pathname.startsWith('/embed/');
    const getLang = () => document.documentElement.getAttribute('lang');
    const getVideoId = () => isEmbeddedVideo() // eslint-disable-line no-confusing-arrow
      ? window.location.pathname.split('/').pop()
      : selectDom('[video-id]').getAttribute('video-id');

    const getButton = memoize(() => selectDom(`#button-${ID_SUFFIX}`));
    const getTooltip = memoize(() => selectDom(`#tooltip-${ID_SUFFIX}`));
    const getMenu = memoize(() => selectDom(`#menu-${ID_SUFFIX}`));
    const getInnerMenu = memoize(() => selectDom(`#inner-menu-${ID_SUFFIX}`));
    const getDownloadLink = memoize(() => selectDom(`#download-link-${ID_SUFFIX}`));
    const getConvertLink = memoize(() => selectDom(`#convert-link-${ID_SUFFIX}`));
    const getAnalyzeLink = memoize(() => selectDom(`#analyze-link-${ID_SUFFIX}`));

    const dict = {
      'en-US': {
        buttonTitle: 'Download via 3D YouTube Downloader',
        download: 'Download',
        convert: 'Convert',
        analyze: 'Analyze',
      },
      'zh-CN': {
        buttonTitle: '通过 3D YouTube Downloader 下载',
        download: '下载',
        convert: '转换',
        analyze: '分析',
      },
    };
    dict.zh = dict['zh-CN'];

    function i18n(key) {
      let lang = getLang();

      // eslint-disable-next-line no-prototype-builtins
      if (!dict.hasOwnProperty(lang)) {
        lang = FALLBACK_LANG;
      }

      const translated = dict[lang][key] || dict[FALLBACK_LANG][key];

      return translated
    }

    function insertControls(youtubeSettingsMenu, youtubeRightControls) {
      const createMenuItem = key => `
<a id="${key}-link-${ID_SUFFIX}" class="ytp-menuitem" tabindex="0">
  <div class="ytp-menuitem-label" style="white-space: nowrap">${i18n(key)}</div>
  <div class="ytp-menuitem-content"></div>
</a>
`;
      const buttonHtml = `
<button id="button-${ID_SUFFIX}" class="ytp-button">
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 459 459" style="transform: scale(0.45)">
    <path fill="#FFF" d="M446.25 56.1l-35.7-43.35C405.45 5.1 395.25 0 382.5 0h-306C63.75 0 53.55 5.1 45.9 12.75L12.75 56.1C5.1 66.3 0 76.5 0 89.25V408c0 28.05 22.95 51 51 51h357c28.05 0 51-22.95 51-51V89.25c0-12.75-5.1-22.95-12.75-33.15zM229.5 369.75L89.25 229.5h89.25v-51h102v51h89.25L229.5 369.75zM53.55 51l20.4-25.5h306L402.9 51H53.55z"/>
  </svg>
</button>
`;
      const tooltipHtml = `
<div id="tooltip-${ID_SUFFIX}" class="ytp-tooltip ytp-bottom" style="opacity: 0">
  <div class="ytp-tooltip-bg">
    <div class="ytp-tooltip-duration"></div>
  </div>
  <div class="ytp-tooltip-text-wrapper">
    <div class="ytp-tooltip-image"></div>
    <div class="ytp-tooltip-title"></div>
    <span class="ytp-tooltip-text">${i18n('buttonTitle')}</span>
  </div>
</div>
`;
      const menuHtml = `
<div id="menu-${ID_SUFFIX}" class="ytp-popup ytp-settings-menu" style="display: none">
  <div class="ytp-panel">
    <div id="inner-menu-${ID_SUFFIX}" class="ytp-panel-menu" style="min-width: 8em" role="menu">
      ${createMenuItem('download')}
      ${createMenuItem('convert')}
      ${createMenuItem('analyze')}
    </div>
  </div>
</div>
`;

      youtubeSettingsMenu.insertAdjacentHTML('beforebegin', menuHtml);
      youtubeSettingsMenu.insertAdjacentHTML('beforebegin', tooltipHtml);
      youtubeRightControls.insertAdjacentHTML('afterbegin', buttonHtml);
    }

    function adjustPosition(element) {
      element.style.right = '0';

      const elementRect = element.getBoundingClientRect();
      const buttonRect = getButton().getBoundingClientRect();
      const youtubeSettingsMenuStyle = getComputedStyle(selectDom('.ytp-settings-menu[id^="ytp-"]'));

      const elementCenterX = elementRect.x + elementRect.width / 2;
      const buttonCenterX = buttonRect.x + buttonRect.width / 2;
      const diff = elementCenterX - buttonCenterX;
      const youtubeSettingsMenuRight = parseInt(youtubeSettingsMenuStyle.right, 10);

      element.style.right = Math.max(diff, youtubeSettingsMenuRight) + 'px';
    }

    function showTooltip() {
      if (isTooltipShown) return
      isTooltipShown = true;

      getTooltip().style.opacity = '1';
      adjustPosition(getTooltip());

      getMenu().style.display = '';
      getTooltip().style.bottom = getComputedStyle(getMenu()).bottom;
      getMenu().style.display = 'none';
    }

    function hideTooltip() {
      if (!isTooltipShown) return
      isTooltipShown = false;

      getTooltip().style.opacity = '0';
    }

    function setDownloadUrls() {
      const videoId = getVideoId();
      const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;

      getDownloadLink().href = `s3dyd://download=${videoUrl}`;
      getConvertLink().href = `s3dyd://convert=${videoUrl}`;
      getAnalyzeLink().href = `s3dyd://analyze=${videoUrl}`;
    }

    function setMenuSize(width, height) {
      width += 'px';
      height += 'px';

      Object.assign(getInnerMenu().parentElement.style, { width, height });
      Object.assign(getMenu().style, { width, height });
    }

    function showMenu() {
      if (isMenuOpen) return
      isMenuOpen = true;

      getMenu().style.opacity = '1';
      getMenu().style.display = '';

      const { offsetWidth: width, offsetHeight: height } = getInnerMenu();

      setMenuSize(width, height);
      setDownloadUrls();
      adjustMenuPosition();
    }

    function adjustMenuPosition() {
      adjustPosition(getMenu());
    }

    function hideMenu() {
      if (!isMenuOpen) return
      isMenuOpen = false;

      getMenu().style.opacity = '0';
      getMenu().addEventListener(
        'transitionend',
        event => {
          if (event.propertyName === 'opacity' && getMenu().style.opacity === '0') {
            getMenu().style.display = 'none';
            getMenu().style.opacity = '';
          }
        },
        { once: true },
      );
    }

    function bindEventHandlers() {
      getButton().addEventListener('click', () => {
        if (isMenuOpen) {
          return
        }

        justOpenedMenu = true;

        hideTooltip();
        showMenu();
      });

      getButton().addEventListener('contextmenu', event => {
        event.preventDefault();
        event.stopPropagation();

        hideTooltip();
        hideMenu();

        setDownloadUrls();
        getDownloadLink().click();
      });

      getButton().addEventListener('mouseenter', () => {
        if (!isMenuOpen) {
          showTooltip();
        }
      });

      getButton().addEventListener('mouseleave', () => {
        if (!isMenuOpen) {
          hideTooltip();
        }
      });

      window.addEventListener('click', () => {
        if (isMenuOpen && !justOpenedMenu) {
          hideMenu();
        }

        justOpenedMenu = false;
      });

      window.addEventListener('blur', () => {
        if (isMenuOpen) {
          hideMenu();
        }
      });
    }

    async function init() {
      if (!isWindowsOS()) {
        return
      }

      const [ youtubeSettingsMenu, youtubeRightControls ] = await Promise.all([
        elementReady_1('.ytp-settings-menu'),
        elementReady_1('.ytp-right-controls'),
      ]);

      if (youtubeSettingsMenu && youtubeRightControls) {
        insertControls(youtubeSettingsMenu, youtubeRightControls);
        bindEventHandlers();
      }
    }
    init();

}());

QingJ © 2025

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