youtube-short-to-long

youtube auto short video jmp to long video

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

})();

QingJ © 2025

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