3D Youtube Downloader Helper

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

目前為 2019-10-02 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

}());