youtube-short-to-long

youtube auto short video jmp to long video

  1. // ==UserScript==
  2. // @name youtube-short-to-long
  3. // @namespace npm/vite-plugin-monkey
  4. // @version 1.1.2
  5. // @author hzx
  6. // @description youtube auto short video jmp to long video
  7. // @license MIT
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  9. // @match https://www.youtube.com/*
  10. // @grant GM_addStyle
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_unregisterMenuCommand
  13. // ==/UserScript==
  14.  
  15. (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} ");
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. const CornMenuManager = /* @__PURE__ */ (() => {
  21. const LOG_TAG = "CornMenuManager: ";
  22. let isLog = true;
  23. const callStack = [];
  24. function getCallStackString() {
  25. const sep = "\n ";
  26. return `callback:${sep}` + callStack.join(sep);
  27. }
  28. function log(msg, logMethod = console.log) {
  29. if (isLog) {
  30. logMethod(LOG_TAG + msg + "\n" + getCallStackString());
  31. }
  32. }
  33. function logError(msg) {
  34. log(msg, console.error);
  35. }
  36. function logWarn(msg) {
  37. log(msg, console.warn);
  38. }
  39. function logWrapper(fnName, fn) {
  40. return () => {
  41. callStack.push(fnName);
  42. const result = fn(fnName);
  43. callStack.pop();
  44. return result;
  45. };
  46. }
  47. function logWrapperAndCall(fnName, fn) {
  48. return logWrapper(fnName, fn)();
  49. }
  50. function isSwitchEntry(item) {
  51. return item && item.on && item.off;
  52. }
  53. const list = [];
  54. const idArr = [];
  55. const STORE_TAG = "MENU_MANAGER_STORE_TAG.";
  56. function setValue(key, value) {
  57. localStorage.setItem(STORE_TAG + key, value);
  58. }
  59. function getValue(key) {
  60. return localStorage.getItem(STORE_TAG + key);
  61. }
  62. function saveSwitchBooleanState(entry, state) {
  63. setValue(getEntryName(entry), state);
  64. }
  65. function getSwitchBooleanState(entry) {
  66. const storeValue = getValue(getEntryName(entry));
  67. if (storeValue === null) {
  68. return null;
  69. }
  70. return storeValue === "true";
  71. }
  72. function getEntryName(entry) {
  73. return entry["name"] || entry["on"]["name"] + entry["off"]["name"];
  74. }
  75. function addEntry(entry) {
  76. logWrapper("addEntry(entry)", (fnName) => {
  77. if (!(typeof entry === "object")) {
  78. logError(`${fnName}: 请传入正确的 Menu Entry`);
  79. return;
  80. }
  81. if (!entry.callback) {
  82. logError(`${fnName}: callback 不能为空, 请传入正确的 Menu Entry`);
  83. return;
  84. }
  85. const nameEmptyHandler = () => {
  86. logError(`${fnName}: entry name 不能为空`);
  87. };
  88. if (isSwitchEntry(entry)) {
  89. if (!entry.on.name || !entry.off.name) {
  90. nameEmptyHandler();
  91. return;
  92. }
  93. if (entry.default === void 0) {
  94. entry.default = true;
  95. }
  96. let currState = getSwitchBooleanState(entry);
  97. if (currState === null) {
  98. saveSwitchBooleanState(entry, entry.default);
  99. currState = entry.default;
  100. }
  101. entry.callback(currState, true);
  102. if (currState) {
  103. entry.currEntry = entry.on;
  104. } else {
  105. entry.currEntry = entry.off;
  106. }
  107. entry.on.next = entry.off;
  108. entry.off.next = entry.on;
  109. } else {
  110. if (!entry.name) {
  111. nameEmptyHandler();
  112. return;
  113. }
  114. }
  115. list.push(entry);
  116. })();
  117. }
  118. function add(entries) {
  119. logWrapper("add(entries)", () => {
  120. if (!Array.isArray(entries)) {
  121. logError("add: 请传递数组, 添加单个请使用 addItem ");
  122. }
  123. for (const entry of entries) {
  124. addEntry(entry);
  125. }
  126. })();
  127. }
  128. return {
  129. // 创建菜单
  130. create(isInit = true) {
  131. logWrapper("create", (fnName) => {
  132. if (list.length === 0) {
  133. logWarn(`${fnName}: 未添加任何 要创建的菜单条目`);
  134. return;
  135. }
  136. for (const id of idArr) {
  137. GM_unregisterMenuCommand(id);
  138. }
  139. idArr.length = 0;
  140. list.forEach((entry, index) => {
  141. let targetName = entry.name;
  142. if (isSwitchEntry(entry)) {
  143. targetName = entry.currEntry.name;
  144. }
  145. const id = GM_registerMenuCommand(targetName, () => {
  146. if (isSwitchEntry(entry)) {
  147. entry.currEntry = entry.currEntry.next;
  148. let currValue = getSwitchBooleanState(entry);
  149. currValue = !currValue;
  150. saveSwitchBooleanState(entry, currValue);
  151. entry.callback(currValue, false);
  152. this.create(false);
  153. } else {
  154. entry.callback();
  155. }
  156. }, entry.accessKey || null);
  157. idArr.push(id);
  158. });
  159. })();
  160. return this;
  161. },
  162. // 添加要创建的菜单项
  163. add(entryOrEntries) {
  164. logWrapperAndCall("add(entryOrEntries)", () => {
  165. if (Array.isArray(entryOrEntries)) {
  166. add(entryOrEntries);
  167. } else {
  168. addEntry(entryOrEntries);
  169. }
  170. });
  171. return this;
  172. },
  173. addAndCreate(entryOrEntries) {
  174. logWrapperAndCall("addAndCreate(entryOrEntries)", () => {
  175. this.add(entryOrEntries);
  176. this.create();
  177. });
  178. return this;
  179. },
  180. disableLog() {
  181. isLog = false;
  182. return this;
  183. }
  184. };
  185. })();
  186. const elmGetter = function() {
  187. const win = window.unsafeWindow || document.defaultView || window;
  188. const doc = win.document;
  189. const listeners = /* @__PURE__ */ new WeakMap();
  190. let mode = "css";
  191. let $;
  192. const elProto = win.Element.prototype;
  193. const matches = elProto.matches || elProto.matchesSelector || elProto.webkitMatchesSelector || elProto.mozMatchesSelector || elProto.oMatchesSelector;
  194. const MutationObs = win.MutationObserver || win.WebkitMutationObserver || win.MozMutationObserver;
  195. function addObserver(target, callback) {
  196. const observer = new MutationObs((mutations) => {
  197. for (const mutation of mutations) {
  198. if (mutation.type === "attributes") {
  199. callback(mutation.target, "attr");
  200. if (observer.canceled) return;
  201. }
  202. for (const node of mutation.addedNodes) {
  203. if (node instanceof Element) callback(node, "insert");
  204. if (observer.canceled) return;
  205. }
  206. }
  207. });
  208. observer.canceled = false;
  209. observer.observe(target, { childList: true, subtree: true, attributes: true, attributeOldValue: true });
  210. return () => {
  211. observer.canceled = true;
  212. observer.disconnect();
  213. };
  214. }
  215. function addFilter(target, filter) {
  216. let listener = listeners.get(target);
  217. if (!listener) {
  218. listener = {
  219. filters: /* @__PURE__ */ new Set(),
  220. remove: addObserver(target, (el, reason) => listener.filters.forEach((f) => f(el, reason)))
  221. };
  222. listeners.set(target, listener);
  223. }
  224. listener.filters.add(filter);
  225. }
  226. function removeFilter(target, filter) {
  227. const listener = listeners.get(target);
  228. if (!listener) return;
  229. listener.filters.delete(filter);
  230. if (!listener.filters.size) {
  231. listener.remove();
  232. listeners.delete(target);
  233. }
  234. }
  235. function query(selector, options = {}) {
  236. let {
  237. parent,
  238. root,
  239. curMode,
  240. reason
  241. } = options;
  242. switch (curMode) {
  243. case "css": {
  244. if (reason === "attr") return matches.call(parent, selector) ? parent : null;
  245. const checkParent = parent !== root && matches.call(parent, selector);
  246. return checkParent ? parent : parent.querySelector(selector);
  247. }
  248. case "jquery": {
  249. if (reason === "attr") return $(parent).is(selector) ? $(parent) : null;
  250. const jNodes = $(parent !== root ? parent : []).add([...parent.querySelectorAll("*")]).filter(selector);
  251. return jNodes.length ? $(jNodes.get(0)) : null;
  252. }
  253. case "xpath": {
  254. const ownerDoc = parent.ownerDocument || parent;
  255. selector += "/self::*";
  256. return ownerDoc.evaluate(selector, reason === "attr" ? root : parent, null, 9, null).singleNodeValue;
  257. }
  258. }
  259. }
  260. function queryAll(selector, options = {}) {
  261. let {
  262. parent,
  263. root,
  264. curMode,
  265. reason
  266. } = options;
  267. switch (curMode) {
  268. case "css": {
  269. if (reason === "attr") return matches.call(parent, selector) ? [parent] : [];
  270. const checkParent = parent !== root && matches.call(parent, selector);
  271. const result = parent.querySelectorAll(selector);
  272. return checkParent ? [parent, ...result] : [...result];
  273. }
  274. case "jquery": {
  275. if (reason === "attr") return $(parent).is(selector) ? [$(parent)] : [];
  276. const jNodes = $(parent !== root ? parent : []).add([...parent.querySelectorAll("*")]).filter(selector);
  277. return $.map(jNodes, (el) => $(el));
  278. }
  279. case "xpath": {
  280. const ownerDoc = parent.ownerDocument || parent;
  281. selector += "/self::*";
  282. const xPathResult = ownerDoc.evaluate(selector, reason === "attr" ? root : parent, null, 7, null);
  283. const result = [];
  284. for (let i = 0; i < xPathResult.snapshotLength; i++) {
  285. result.push(xPathResult.snapshotItem(i));
  286. }
  287. return result;
  288. }
  289. }
  290. }
  291. function isJquery(jq) {
  292. return jq && jq.fn && typeof jq.fn.jquery === "string";
  293. }
  294. function getOne(selector, options = {}) {
  295. let {
  296. parent,
  297. timeout,
  298. onError,
  299. isPending,
  300. errEl: errEl2
  301. } = options;
  302. const curMode = mode;
  303. return new Promise((resolve) => {
  304. const node = query(
  305. selector,
  306. {
  307. parent,
  308. root: parent,
  309. curMode
  310. }
  311. );
  312. if (node) return resolve(node);
  313. let timer;
  314. const filter = (el, reason) => {
  315. const node2 = query(
  316. selector,
  317. {
  318. parent,
  319. root: parent,
  320. curMode
  321. }
  322. );
  323. if (node2) {
  324. removeFilter(parent, filter);
  325. timer && clearTimeout(timer);
  326. resolve(node2);
  327. }
  328. };
  329. addFilter(parent, filter);
  330. if (timeout > 0) {
  331. timer = setTimeout(() => {
  332. removeFilter(parent, filter);
  333. onError(selector);
  334. if (!isPending) {
  335. resolve(errEl2);
  336. }
  337. }, timeout);
  338. }
  339. });
  340. }
  341. let errEl = document.createElement("div");
  342. errEl.classList.add("no-found");
  343. errEl.remove = () => {
  344. };
  345. return {
  346. timeout: 0,
  347. onError: (selector) => {
  348. console.warn(`[elmGetter] [get失败] selector为: ${selector} 的查询超时`);
  349. },
  350. isPending: true,
  351. errEl,
  352. get currentSelector() {
  353. return mode;
  354. },
  355. /**
  356. * 异步的 querySelector
  357. * @param selector
  358. * @param options 一个对象
  359. * - parent 父元素, 默认值是 document
  360. * - timeout 设置 get 的超时时间, 默认值是 elmGetter.timeout, 其值默认为 0
  361. * - 如果该值为 0, 表示永不超时, 如果 selector 有误, 返回的 Promise 将永远 pending
  362. * - 如果该值不为 0, 表示等待多少毫秒, 和 setTimeout 单位一致
  363. * - onError 超时后的失败回调, 参数为 selector, 默认值为 elmGetter.onError, 其默认行为是 console.warn 打印 selector
  364. * - isPending 超时后 Promise 是否仍然保持 pending, 默认值为 elmGetter.isPending, 其值默认为 true
  365. * - errEl 超时后 Promise 返回的值, 需要 isPending 为 false 才能有效, 默认值为 elmGetter.errorEl, 其值默认为一个 class 为一个 class 为 no-found 的元素
  366. * @returns {Promise<Awaited<unknown>[]>|Promise<unknown>}
  367. */
  368. get(selector, options = {}) {
  369. let {
  370. parent = doc,
  371. timeout = this.timeout,
  372. onError = this.onError,
  373. isPending = this.isPending,
  374. errEl: errEl2 = this.errEl
  375. } = options;
  376. options.parent = parent;
  377. options.timeout = timeout;
  378. options.onError = onError;
  379. options.isPending = isPending;
  380. options.errEl = errEl2;
  381. if (mode === "jquery" && parent instanceof $) parent = parent.get(0);
  382. if (Array.isArray(selector)) {
  383. return Promise.all(selector.map((s) => getOne(s, options)));
  384. }
  385. return getOne(selector, options);
  386. },
  387. /**
  388. * 为父节点设置监听,所有符合选择器的元素(包括页面已有的和新插入的)都将被传给回调函数处理,
  389. * each方法适用于各种滚动加载的列表(如评论区),或者发生非刷新跳转的页面等
  390. * @param selector
  391. * @param callback 回调函数, 只在每个元素上触发一次。 回调函数接收2个参数,第一个是符合选择器的元素,第二个表明该元素是否为新插入的(已有为false,插入为true)
  392. * @param options 一个对象
  393. * - parent 父元素, 默认值是 document
  394. */
  395. each(selector, callback, options = {}) {
  396. let {
  397. parent = doc
  398. } = options;
  399. if (mode === "jquery" && parent instanceof $) parent = parent.get(0);
  400. const curMode = mode;
  401. const refs = /* @__PURE__ */ new WeakSet();
  402. for (const node of queryAll(selector, { parent, root: parent, curMode })) {
  403. refs.add(curMode === "jquery" ? node.get(0) : node);
  404. if (callback(node, false) === false) return;
  405. }
  406. const filter = (el, reason) => {
  407. for (const node of queryAll(selector, { parent: el, root: parent, curMode, reason })) {
  408. const _el = curMode === "jquery" ? node.get(0) : node;
  409. if (refs.has(_el)) break;
  410. refs.add(_el);
  411. if (callback(node, true) === false) {
  412. return removeFilter(parent, filter);
  413. }
  414. }
  415. };
  416. addFilter(parent, filter);
  417. },
  418. /**
  419. * 将html字符串解析为元素
  420. * @param domString
  421. * @param options 一个对象
  422. * - returnList 布尔值,是否返回以 id 作为索引的元素列表, 默认值为 false
  423. * - parent 父节点,将创建的元素添加到父节点末尾处, 如果不指定, 解析后的元素将
  424. * @returns {Element|{}|null} 元素或对象,取决于returnList参数
  425. */
  426. create(domString, options = {}) {
  427. let {
  428. returnList = false,
  429. parent = null
  430. } = options;
  431. const template = doc.createElement("template");
  432. template.innerHTML = domString;
  433. const node = template.content.firstElementChild;
  434. if (!node) return null;
  435. parent ? parent.appendChild(node) : node.remove();
  436. if (returnList) {
  437. const list = {};
  438. node.querySelectorAll("[id]").forEach((el) => list[el.id] = el);
  439. list[0] = node;
  440. return list;
  441. }
  442. return node;
  443. },
  444. selector(desc) {
  445. switch (true) {
  446. case isJquery(desc):
  447. $ = desc;
  448. return mode = "jquery";
  449. case (!desc || typeof desc.toLowerCase !== "function"):
  450. return mode = "css";
  451. case desc.toLowerCase() === "jquery":
  452. for (const jq of [window.jQuery, window.$, win.jQuery, win.$]) {
  453. if (isJquery(jq)) {
  454. $ = jq;
  455. break;
  456. }
  457. }
  458. return mode = $ ? "jquery" : "css";
  459. case desc.toLowerCase() === "xpath":
  460. return mode = "xpath";
  461. default:
  462. return mode = "css";
  463. }
  464. }
  465. };
  466. }();
  467. async function main() {
  468. createMenu();
  469. }
  470. main();
  471. function createMenu() {
  472. CornMenuManager.addAndCreate([
  473. {
  474. default: true,
  475. callback(state, isInit) {
  476. if (!isInit) {
  477. location.reload();
  478. }
  479. if (!state) {
  480. return;
  481. }
  482. async function processState() {
  483. await elmGetter.each(".shortsLockupViewModelHostEndpoint", (el) => {
  484. el.href = convertShortsToVideoLink(el.href);
  485. });
  486. await elmGetter.each("ytm-shorts-lockup-view-model-v2", (el) => {
  487. let mask = document.createElement("a");
  488. mask.className = "mask";
  489. el.appendChild(mask);
  490. const aEl = el.querySelector(`a`);
  491. mask.href = aEl.href;
  492. });
  493. }
  494. jmpToVideo();
  495. processState();
  496. },
  497. on: {
  498. name: "自动跳转状态: 开启✅ (点我关闭)"
  499. },
  500. off: {
  501. name: "自动跳转状态: 关闭❎ (点我开启)"
  502. }
  503. },
  504. {
  505. name: "跳转到 Video",
  506. callback() {
  507. jmpToVideo();
  508. }
  509. }
  510. ]);
  511. }
  512. function convertShortsToVideoLink(shortsUrl) {
  513. if (shortsUrl.toLowerCase().includes("/shorts/")) {
  514. return shortsUrl.replace("/shorts/", "/watch?v=");
  515. } else {
  516. return shortsUrl;
  517. }
  518. }
  519. function jmpToVideo() {
  520. const href = window.location.href;
  521. if (href.toLowerCase().includes("/shorts/")) {
  522. window.location.href = convertShortsToVideoLink(href);
  523. }
  524. }
  525.  
  526. })();

QingJ © 2025

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