youtube-short-to-long

youtube auto short video jmp to long video

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         youtube-short-to-long
// @namespace    npm/vite-plugin-monkey
// @version      1.1.2
// @author       hzx
// @description  youtube auto short video jmp to long video
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @match        https://www.youtube.com/*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// ==/UserScript==

(t=>{if(typeof GM_addStyle=="function"){GM_addStyle(t);return}const e=document.createElement("style");e.textContent=t,document.head.append(e)})(" .mask{position:absolute;width:100%;height:100%;background-color:transparent;top:0;right:0;left:0;bottom:0;cursor:pointer} ");

(function () {
  'use strict';

  const CornMenuManager = /* @__PURE__ */ (() => {
    const LOG_TAG = "CornMenuManager: ";
    let isLog = true;
    const callStack = [];
    function getCallStackString() {
      const sep = "\n	";
      return `callback:${sep}` + callStack.join(sep);
    }
    function log(msg, logMethod = console.log) {
      if (isLog) {
        logMethod(LOG_TAG + msg + "\n" + getCallStackString());
      }
    }
    function logError(msg) {
      log(msg, console.error);
    }
    function logWarn(msg) {
      log(msg, console.warn);
    }
    function logWrapper(fnName, fn) {
      return () => {
        callStack.push(fnName);
        const result = fn(fnName);
        callStack.pop();
        return result;
      };
    }
    function logWrapperAndCall(fnName, fn) {
      return logWrapper(fnName, fn)();
    }
    function isSwitchEntry(item) {
      return item && item.on && item.off;
    }
    const list = [];
    const idArr = [];
    const STORE_TAG = "MENU_MANAGER_STORE_TAG.";
    function setValue(key, value) {
      localStorage.setItem(STORE_TAG + key, value);
    }
    function getValue(key) {
      return localStorage.getItem(STORE_TAG + key);
    }
    function saveSwitchBooleanState(entry, state) {
      setValue(getEntryName(entry), state);
    }
    function getSwitchBooleanState(entry) {
      const storeValue = getValue(getEntryName(entry));
      if (storeValue === null) {
        return null;
      }
      return storeValue === "true";
    }
    function getEntryName(entry) {
      return entry["name"] || entry["on"]["name"] + entry["off"]["name"];
    }
    function addEntry(entry) {
      logWrapper("addEntry(entry)", (fnName) => {
        if (!(typeof entry === "object")) {
          logError(`${fnName}: 请传入正确的 Menu Entry`);
          return;
        }
        if (!entry.callback) {
          logError(`${fnName}: callback 不能为空, 请传入正确的 Menu Entry`);
          return;
        }
        const nameEmptyHandler = () => {
          logError(`${fnName}: entry name 不能为空`);
        };
        if (isSwitchEntry(entry)) {
          if (!entry.on.name || !entry.off.name) {
            nameEmptyHandler();
            return;
          }
          if (entry.default === void 0) {
            entry.default = true;
          }
          let currState = getSwitchBooleanState(entry);
          if (currState === null) {
            saveSwitchBooleanState(entry, entry.default);
            currState = entry.default;
          }
          entry.callback(currState, true);
          if (currState) {
            entry.currEntry = entry.on;
          } else {
            entry.currEntry = entry.off;
          }
          entry.on.next = entry.off;
          entry.off.next = entry.on;
        } else {
          if (!entry.name) {
            nameEmptyHandler();
            return;
          }
        }
        list.push(entry);
      })();
    }
    function add(entries) {
      logWrapper("add(entries)", () => {
        if (!Array.isArray(entries)) {
          logError("add: 请传递数组, 添加单个请使用 addItem ");
        }
        for (const entry of entries) {
          addEntry(entry);
        }
      })();
    }
    return {
      // 创建菜单
      create(isInit = true) {
        logWrapper("create", (fnName) => {
          if (list.length === 0) {
            logWarn(`${fnName}: 未添加任何 要创建的菜单条目`);
            return;
          }
          for (const id of idArr) {
            GM_unregisterMenuCommand(id);
          }
          idArr.length = 0;
          list.forEach((entry, index) => {
            let targetName = entry.name;
            if (isSwitchEntry(entry)) {
              targetName = entry.currEntry.name;
            }
            const id = GM_registerMenuCommand(targetName, () => {
              if (isSwitchEntry(entry)) {
                entry.currEntry = entry.currEntry.next;
                let currValue = getSwitchBooleanState(entry);
                currValue = !currValue;
                saveSwitchBooleanState(entry, currValue);
                entry.callback(currValue, false);
                this.create(false);
              } else {
                entry.callback();
              }
            }, entry.accessKey || null);
            idArr.push(id);
          });
        })();
        return this;
      },
      // 添加要创建的菜单项
      add(entryOrEntries) {
        logWrapperAndCall("add(entryOrEntries)", () => {
          if (Array.isArray(entryOrEntries)) {
            add(entryOrEntries);
          } else {
            addEntry(entryOrEntries);
          }
        });
        return this;
      },
      addAndCreate(entryOrEntries) {
        logWrapperAndCall("addAndCreate(entryOrEntries)", () => {
          this.add(entryOrEntries);
          this.create();
        });
        return this;
      },
      disableLog() {
        isLog = false;
        return this;
      }
    };
  })();
  const elmGetter = function() {
    const win = window.unsafeWindow || document.defaultView || window;
    const doc = win.document;
    const listeners = /* @__PURE__ */ new WeakMap();
    let mode = "css";
    let $;
    const elProto = win.Element.prototype;
    const matches = elProto.matches || elProto.matchesSelector || elProto.webkitMatchesSelector || elProto.mozMatchesSelector || elProto.oMatchesSelector;
    const MutationObs = win.MutationObserver || win.WebkitMutationObserver || win.MozMutationObserver;
    function addObserver(target, callback) {
      const observer = new MutationObs((mutations) => {
        for (const mutation of mutations) {
          if (mutation.type === "attributes") {
            callback(mutation.target, "attr");
            if (observer.canceled) return;
          }
          for (const node of mutation.addedNodes) {
            if (node instanceof Element) callback(node, "insert");
            if (observer.canceled) return;
          }
        }
      });
      observer.canceled = false;
      observer.observe(target, { childList: true, subtree: true, attributes: true, attributeOldValue: true });
      return () => {
        observer.canceled = true;
        observer.disconnect();
      };
    }
    function addFilter(target, filter) {
      let listener = listeners.get(target);
      if (!listener) {
        listener = {
          filters: /* @__PURE__ */ new Set(),
          remove: addObserver(target, (el, reason) => listener.filters.forEach((f) => f(el, reason)))
        };
        listeners.set(target, listener);
      }
      listener.filters.add(filter);
    }
    function removeFilter(target, filter) {
      const listener = listeners.get(target);
      if (!listener) return;
      listener.filters.delete(filter);
      if (!listener.filters.size) {
        listener.remove();
        listeners.delete(target);
      }
    }
    function query(selector, options = {}) {
      let {
        parent,
        root,
        curMode,
        reason
      } = options;
      switch (curMode) {
        case "css": {
          if (reason === "attr") return matches.call(parent, selector) ? parent : null;
          const checkParent = parent !== root && matches.call(parent, selector);
          return checkParent ? parent : parent.querySelector(selector);
        }
        case "jquery": {
          if (reason === "attr") return $(parent).is(selector) ? $(parent) : null;
          const jNodes = $(parent !== root ? parent : []).add([...parent.querySelectorAll("*")]).filter(selector);
          return jNodes.length ? $(jNodes.get(0)) : null;
        }
        case "xpath": {
          const ownerDoc = parent.ownerDocument || parent;
          selector += "/self::*";
          return ownerDoc.evaluate(selector, reason === "attr" ? root : parent, null, 9, null).singleNodeValue;
        }
      }
    }
    function queryAll(selector, options = {}) {
      let {
        parent,
        root,
        curMode,
        reason
      } = options;
      switch (curMode) {
        case "css": {
          if (reason === "attr") return matches.call(parent, selector) ? [parent] : [];
          const checkParent = parent !== root && matches.call(parent, selector);
          const result = parent.querySelectorAll(selector);
          return checkParent ? [parent, ...result] : [...result];
        }
        case "jquery": {
          if (reason === "attr") return $(parent).is(selector) ? [$(parent)] : [];
          const jNodes = $(parent !== root ? parent : []).add([...parent.querySelectorAll("*")]).filter(selector);
          return $.map(jNodes, (el) => $(el));
        }
        case "xpath": {
          const ownerDoc = parent.ownerDocument || parent;
          selector += "/self::*";
          const xPathResult = ownerDoc.evaluate(selector, reason === "attr" ? root : parent, null, 7, null);
          const result = [];
          for (let i = 0; i < xPathResult.snapshotLength; i++) {
            result.push(xPathResult.snapshotItem(i));
          }
          return result;
        }
      }
    }
    function isJquery(jq) {
      return jq && jq.fn && typeof jq.fn.jquery === "string";
    }
    function getOne(selector, options = {}) {
      let {
        parent,
        timeout,
        onError,
        isPending,
        errEl: errEl2
      } = options;
      const curMode = mode;
      return new Promise((resolve) => {
        const node = query(
          selector,
          {
            parent,
            root: parent,
            curMode
          }
        );
        if (node) return resolve(node);
        let timer;
        const filter = (el, reason) => {
          const node2 = query(
            selector,
            {
              parent,
              root: parent,
              curMode
            }
          );
          if (node2) {
            removeFilter(parent, filter);
            timer && clearTimeout(timer);
            resolve(node2);
          }
        };
        addFilter(parent, filter);
        if (timeout > 0) {
          timer = setTimeout(() => {
            removeFilter(parent, filter);
            onError(selector);
            if (!isPending) {
              resolve(errEl2);
            }
          }, timeout);
        }
      });
    }
    let errEl = document.createElement("div");
    errEl.classList.add("no-found");
    errEl.remove = () => {
    };
    return {
      timeout: 0,
      onError: (selector) => {
        console.warn(`[elmGetter] [get失败] selector为: ${selector} 的查询超时`);
      },
      isPending: true,
      errEl,
      get currentSelector() {
        return mode;
      },
      /**
       * 异步的 querySelector
       * @param selector
       * @param options 一个对象
       *  - parent 父元素, 默认值是 document
       *  - timeout 设置 get 的超时时间, 默认值是 elmGetter.timeout, 其值默认为 0
       *      - 如果该值为 0, 表示永不超时, 如果 selector 有误, 返回的 Promise 将永远 pending
       *      - 如果该值不为 0, 表示等待多少毫秒, 和 setTimeout 单位一致
       *  - onError 超时后的失败回调, 参数为 selector, 默认值为 elmGetter.onError, 其默认行为是 console.warn 打印 selector
       *  - isPending 超时后 Promise 是否仍然保持 pending, 默认值为 elmGetter.isPending, 其值默认为 true
       *  - errEl 超时后 Promise 返回的值, 需要 isPending 为 false 才能有效, 默认值为 elmGetter.errorEl, 其值默认为一个 class 为一个 class 为 no-found 的元素
       * @returns {Promise<Awaited<unknown>[]>|Promise<unknown>}
       */
      get(selector, options = {}) {
        let {
          parent = doc,
          timeout = this.timeout,
          onError = this.onError,
          isPending = this.isPending,
          errEl: errEl2 = this.errEl
        } = options;
        options.parent = parent;
        options.timeout = timeout;
        options.onError = onError;
        options.isPending = isPending;
        options.errEl = errEl2;
        if (mode === "jquery" && parent instanceof $) parent = parent.get(0);
        if (Array.isArray(selector)) {
          return Promise.all(selector.map((s) => getOne(s, options)));
        }
        return getOne(selector, options);
      },
      /**
       * 为父节点设置监听,所有符合选择器的元素(包括页面已有的和新插入的)都将被传给回调函数处理,
       * each方法适用于各种滚动加载的列表(如评论区),或者发生非刷新跳转的页面等
       * @param selector
       * @param callback 回调函数, 只在每个元素上触发一次。 回调函数接收2个参数,第一个是符合选择器的元素,第二个表明该元素是否为新插入的(已有为false,插入为true)
       * @param options 一个对象
       *  - parent 父元素, 默认值是 document
       */
      each(selector, callback, options = {}) {
        let {
          parent = doc
        } = options;
        if (mode === "jquery" && parent instanceof $) parent = parent.get(0);
        const curMode = mode;
        const refs = /* @__PURE__ */ new WeakSet();
        for (const node of queryAll(selector, { parent, root: parent, curMode })) {
          refs.add(curMode === "jquery" ? node.get(0) : node);
          if (callback(node, false) === false) return;
        }
        const filter = (el, reason) => {
          for (const node of queryAll(selector, { parent: el, root: parent, curMode, reason })) {
            const _el = curMode === "jquery" ? node.get(0) : node;
            if (refs.has(_el)) break;
            refs.add(_el);
            if (callback(node, true) === false) {
              return removeFilter(parent, filter);
            }
          }
        };
        addFilter(parent, filter);
      },
      /**
       * 将html字符串解析为元素
       * @param domString
       * @param options 一个对象
       *  - returnList 布尔值,是否返回以 id 作为索引的元素列表, 默认值为 false
       *  - parent 父节点,将创建的元素添加到父节点末尾处, 如果不指定, 解析后的元素将
       * @returns {Element|{}|null} 元素或对象,取决于returnList参数
       */
      create(domString, options = {}) {
        let {
          returnList = false,
          parent = null
        } = options;
        const template = doc.createElement("template");
        template.innerHTML = domString;
        const node = template.content.firstElementChild;
        if (!node) return null;
        parent ? parent.appendChild(node) : node.remove();
        if (returnList) {
          const list = {};
          node.querySelectorAll("[id]").forEach((el) => list[el.id] = el);
          list[0] = node;
          return list;
        }
        return node;
      },
      selector(desc) {
        switch (true) {
          case isJquery(desc):
            $ = desc;
            return mode = "jquery";
          case (!desc || typeof desc.toLowerCase !== "function"):
            return mode = "css";
          case desc.toLowerCase() === "jquery":
            for (const jq of [window.jQuery, window.$, win.jQuery, win.$]) {
              if (isJquery(jq)) {
                $ = jq;
                break;
              }
            }
            return mode = $ ? "jquery" : "css";
          case desc.toLowerCase() === "xpath":
            return mode = "xpath";
          default:
            return mode = "css";
        }
      }
    };
  }();
  async function main() {
    createMenu();
  }
  main();
  function createMenu() {
    CornMenuManager.addAndCreate([
      {
        default: true,
        callback(state, isInit) {
          if (!isInit) {
            location.reload();
          }
          if (!state) {
            return;
          }
          async function processState() {
            await elmGetter.each(".shortsLockupViewModelHostEndpoint", (el) => {
              el.href = convertShortsToVideoLink(el.href);
            });
            await elmGetter.each("ytm-shorts-lockup-view-model-v2", (el) => {
              let mask = document.createElement("a");
              mask.className = "mask";
              el.appendChild(mask);
              const aEl = el.querySelector(`a`);
              mask.href = aEl.href;
            });
          }
          jmpToVideo();
          processState();
        },
        on: {
          name: "自动跳转状态: 开启✅ (点我关闭)"
        },
        off: {
          name: "自动跳转状态: 关闭❎ (点我开启)"
        }
      },
      {
        name: "跳转到 Video",
        callback() {
          jmpToVideo();
        }
      }
    ]);
  }
  function convertShortsToVideoLink(shortsUrl) {
    if (shortsUrl.toLowerCase().includes("/shorts/")) {
      return shortsUrl.replace("/shorts/", "/watch?v=");
    } else {
      return shortsUrl;
    }
  }
  function jmpToVideo() {
    const href = window.location.href;
    if (href.toLowerCase().includes("/shorts/")) {
      window.location.href = convertShortsToVideoLink(href);
    }
  }

})();