🌀VIP视频解析——fatcat

适配手机端与电脑端,可自定义解析网址

  1. // ==UserScript==
  2. // @name 🌀VIP视频解析——fatcat
  3. // @namespace https://greasyfork.org/users/1313123-fei-miao
  4. // @version 2.9.3
  5. // @description 适配手机端与电脑端,可自定义解析网址
  6. // @author 暴走的肥猫
  7. // @icon data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='_5' x1='32' y1='60.96' x2='32' y2='5.18' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%230087c9'/%3E%3Cstop offset='1' stop-color='%238fe36d'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='0' width='64' height='64' rx='13.63' ry='13.63' style='fill:url(%23_5); stroke-width:0px;'/%3E%3Cpath d='M53.14,22.76c1.04-1.28.37-2.32-.1-3.28-7.69-15.14-32.73-14.58-41.2-.5-13.44,19.29,7.69,44.27,28.91,34.61,14.13-6.24,14.32-28.05-.2-33.98-10.14-4.77-23.09,3.29-20.59,14.46,1.05,5.91,7.45,10.54,13.95,8.68,6.93-1.56,7.18-11.47.7-13.57-3.5-1.47-4.42,2.67-1.49,3.99,2.87,2.01.95,5.41-2.19,4.95-13.06-4.66.32-19.8,9.11-12.19,15.65,13.58-7.47,31.84-20.36,18.96C1.06,23.14,32.46.23,53.14,22.76Z' style='fill:%23f8faf9; stroke-width:0px;'/%3E%3C/svg%3E
  8. // @license GNU AGPLv3
  9. // @match *://*.youku.com/*
  10. // @match *://*.iqiyi.com/*
  11. // @match *://v.qq.com/*
  12. // @match *://*.v.qq.com/*
  13. // @match *://*.mgtv.com/*
  14. // @match *://tv.sohu.com/*
  15. // @match *://film.sohu.com/*
  16. // @match *://*.bilibili.com/*
  17. // @match *://*.tudou.com/*
  18. // @match *://*.pptv.com/*
  19. // @compatible safari
  20. // @compatible chrome
  21. // @compatible edge
  22. // @grant GM_getValue
  23. // @grant GM_setValue
  24. // @grant GM.getValue
  25. // @grant GM.setValue
  26. // @run-at document-start
  27. // @noframes
  28. // ==/UserScript==
  29. /*jshint esversion: 11 */
  30. (async function () {
  31. const ACCESS_POINT = [
  32. { "name": "虾米-1", "url": "https://jx.xmflv.com/?url=" },
  33. { "name": "虾米-2", "url": "https://jx.xmflv.cc/?url=" },
  34. { "name": "1907", "url": "https://im1907.top/?jx=" },
  35. { "name": "M3U8TV", "url": "https://jx.m3u8.tv/jiexi/?url=" },
  36. { "name": "夜幕", "url": "https://www.yemu.xyz/?url=" },
  37. { "name": "777", "url": "https://jx.jsonplayer.com/player/?url=" },
  38. { "name": "CK", "url": "https://www.ckplayer.vip/jiexi/?url=" },
  39. { "name": "YT", "url": "https://jx.yangtu.top/?url=" },
  40. { "name": "Player-JY", "url": "https://jx.playerjy.com/?url=" },
  41. { "name": "Yparse", "url": "https://jx.yparse.com/index.php?url=" },
  42. { "name": "8090", "url": "https://www.8090g.cn/?url=" },
  43. { "name": "剖元", "url": "https://www.pouyun.com/?url=" },
  44. { "name": "全民", "url": "https://43.240.74.102:4433?url=" },
  45. { "name": "爱豆", "url": "https://jx.aidouer.net/?url=" },
  46. { "name": "冰豆", "url": "https://bd.jx.cn/?url=" },
  47. { "name": "Playm3u8", "url": "https://www.playm3u8.cn/jiexi.php?url=" },
  48. { "name": "mmkv", "url": "https://jx.mmkv.cn/tv.php?url=" },
  49. ];
  50. var searchSelector = ['video'];
  51. var patches = { 'm.iqiyi.com': () => Object.defineProperty(navigator, 'userAgent', { get: () => 'Android' }) }
  52. async function getUserConfig(key, defaultVal) {
  53. return typeof GM_getValue === 'function' ? GM_getValue(key, defaultVal) : GM.getValue(key, defaultVal);
  54. }
  55. async function setUserConfig(key, val) {
  56. return typeof GM_setValue === 'function' ? GM_setValue(key, val) : GM.setValue(key, val);
  57. }
  58. function seekSameSizeParentNode(node) {
  59. let nodeSize = node.getBoundingClientRect();
  60. let parents = [node];
  61. while (node.parentNode != document.body) {
  62. parents.unshift(node.parentNode);
  63. node = node.parentNode;
  64. };
  65. let parentSize;
  66. let sameSizeParent = parents.find(parent => {
  67. parentSize = parent.getBoundingClientRect();
  68. return Math.abs(parentSize.width - nodeSize.width) < 1 && Math.abs(parentSize.height - nodeSize.height) < 1
  69. });
  70. return sameSizeParent;
  71. }
  72. function useInterval(fn, intervalTime, maxTime) {
  73. return new Promise((resolve) => {
  74. let totalTime = 0;
  75. let interval = setInterval(() => {
  76. totalTime += intervalTime;
  77. if (totalTime >= maxTime || fn()) { clearInterval(interval); resolve(); }
  78. }, intervalTime)
  79. })
  80. }
  81. async function findVideoWrapper() {
  82. await useInterval(() => Array.from(document.querySelectorAll(searchSelector.join(','))).find(videoNode => {
  83. if (window.getComputedStyle(videoNode).display == 'none') videoNode.style.display = 'revert';
  84. return videoNode.getBoundingClientRect().width > 90 && videoNode.getBoundingClientRect().height > 40
  85. }), 200, Infinity);
  86. let maxVideoNode = Array.from(document.querySelectorAll(searchSelector.join(','))).reduce((pre, cur) => {
  87. if (cur.getBoundingClientRect().width > (pre?.getBoundingClientRect().width || 0)) return cur;
  88. return pre;
  89. });
  90. return seekSameSizeParentNode(maxVideoNode);
  91. }
  92. async function createVideoFrame(videoWrapper, curAccessPoint) {
  93. videoWrapper.style.overflow = 'hidden';
  94. if (window.getComputedStyle(videoWrapper).position == 'static') videoWrapper.style.position = 'relative';
  95. let iframeWrapper = document.createElement('div');
  96. iframeWrapper.innerHTML = `<iframe src="${curAccessPoint ? curAccessPoint.url + window.location.href : ''}" width="100%" height="100%" allowfullscreen id="fatcat_video_vip_iframe" style="border:none;"/>`;
  97. iframeWrapper.style = `position:absolute; inset:0; z-index: 99999999;`
  98. videoWrapper.appendChild(iframeWrapper);
  99. Array.from(videoWrapper.children).forEach(el => { if (el != iframeWrapper) el.style.visibility = 'hidden' })
  100. return;
  101. }
  102. async function createMenu(accessPoints, curAccessPoint) {
  103. if (document.querySelector('#fatcat_video_vip')) return;
  104. let wrapper = document.createElement('div');
  105. wrapper.innerHTML = `
  106. <div id="fatcat_video_vip" style="bottom: 8px; right: 8px;">
  107. <style>
  108. #fatcat_video_vip {
  109. position: fixed;
  110. z-index: 99999999;
  111. }
  112. #fatcat_video_vip *{
  113. transition: all ease 0.2s;
  114. }
  115. #fatcat_video_vip button {
  116. all: unset;
  117. }
  118. #fatcat_video_vip :is(.block-btn, .fatcat_video_vip_link) {
  119. display: inline-block;
  120. vertical-align: middle;
  121. background: linear-gradient(to top right, #61c400bf 0%, #2ea5ffc2 100%);
  122. white-space: nowrap;
  123. width: auto;
  124. height: 33px;
  125. padding-inline: 10px;
  126. font-size: 13px;
  127. color: white;
  128. border-radius: 8px;
  129. text-align: center;
  130. outline: none;
  131. border: 1px solid #ffffff57;
  132. backdrop-filter: blur(5px);
  133. -webkit-backdrop-filter: blur(5px);
  134. cursor: pointer;
  135. }
  136. #fatcat_video_vip_popover {
  137. background: #191a20eb;
  138. font-size: 15px;
  139. margin: auto;
  140. border-radius: 20px;
  141. padding: 20px;
  142. border: none;
  143. position: fixed;
  144. bottom: 8px;
  145. right: 8px;
  146. max-width: clac(100vw - 10px);
  147. color: white;
  148. border: 1px solid #ffffff42;
  149. backdrop-filter: blur(5px);
  150. -webkit-backdrop-filter: blur(5px);
  151. white-space: nowrap;
  152. }
  153. #fatcat_video_vip_popover input{
  154. display: inline-block;
  155. width: 60px;
  156. height: 33px;
  157. color: rgba(255,255,255,0.6);
  158. background-color: #191a20eb!important;
  159. border: 1px solid #ffffff42;
  160. padding: 0 10px;
  161. }
  162. #fatcat_video_vip .fatcat_video_vip_link{
  163. width: 17px;
  164. height: 17px;
  165. line-height: 17px;
  166. font-size: 10px;
  167. padding: 0;
  168. }
  169. #fatcat_video_vip_link{
  170. display: inline-block;
  171. color: inherit;
  172. text-decoration: none;
  173. }
  174. #fatcat_video_vip_link:hover{
  175. color: rgba(255,255,255,0.8);
  176. transform: translateX(2px);
  177. }
  178. #fatcat_video_vip_link:hover .fatcat_video_vip_link{
  179. width: 50px;
  180. }
  181. </style>
  182. <button class="block-btn" id="fatcat_video_vip_setwrapper">📍自选视频位置</button>
  183. <button class="block-btn" style="font-size: 20px;" id="fatcat_video_vip_popover_toggle_btn">≡</button>
  184. <div id="fatcat_video_vip_popover" hidden>
  185. <p style="margin-block: 0 16px;"><a href="${curAccessPoint ? curAccessPoint.url + window.location.href : ''}" target="_blank" id="fatcat_video_vip_link"><b id="fatcat_video_vip_popover_title">当前解析点 <span class="fatcat_video_vip_link">➜</span></b></a></p>
  186. <div style="display:flex;gap: 16px;">
  187. <select class="block-btn" id="fatcat_video_vip_select" style="background: #191a20eb;width:170px;text-align:left;">
  188. ${accessPoints.map(({ name, url }) => `<option value="${url}" ${name == curAccessPoint?.name ? 'selected' : ''}>${name}</option>`).join()}
  189. </select>
  190. <button class="block-btn" id="fatcat_video_vip_delpoint">删除</button>
  191. <button class="block-btn" id="fatcat_video_vip_resetpoints">重置</button>
  192. </div>
  193. <p style="margin-block: 20px 16px;"><b>自定义解析点</b></p>
  194. <div style="display:flex;gap: 16px;">
  195. <input id="fatcat_video_vip_point_name" placeholder="名称" autocomplete="off" style="border-radius: 8px 0 0 8px;"/>
  196. <input id="fatcat_video_vip_point_url" type="url" placeholder="URL" autocomplete="off" style="border-radius: 0 8px 8px 0;margin-left:-17px;width:140px;flex:1;"/>
  197. <button class="block-btn" id="fatcat_video_vip_addpoint">增加</button>
  198. </div>
  199. <button style="position:absolute; top:6px; right:10px; opacity: 0.5; cursor:pointer; padding:10px;" id="fatcat_video_vip_popover_close_btn">✕</button>
  200. </div>
  201. </div>
  202. `;
  203. let popoverToggleBtn = wrapper.querySelector('#fatcat_video_vip_popover_toggle_btn');
  204. let popoverCloseBtn = wrapper.querySelector('#fatcat_video_vip_popover_close_btn');
  205. let popover = wrapper.querySelector('#fatcat_video_vip_popover');
  206. popoverToggleBtn.onclick = () => popover.toggleAttribute('hidden');
  207. popoverCloseBtn.onclick = () => popover.toggleAttribute('hidden', true);
  208. window.addEventListener('click', (e) => { if (!e.target.matches('#fatcat_video_vip_popover,#fatcat_video_vip_popover *,#fatcat_video_vip_popover_toggle_btn')) popover.toggleAttribute('hidden', true); })
  209. let popoverTitle = wrapper.querySelector('#fatcat_video_vip_popover_title');
  210. let nameInput = wrapper.querySelector('#fatcat_video_vip_point_name');
  211. let urlInput = wrapper.querySelector('#fatcat_video_vip_point_url');
  212. let addBtn = wrapper.querySelector('#fatcat_video_vip_addpoint');
  213. let delBtn = wrapper.querySelector('#fatcat_video_vip_delpoint');
  214. let setWrapperBtn = wrapper.querySelector('#fatcat_video_vip_setwrapper');
  215. let resetBtn = wrapper.querySelector('#fatcat_video_vip_resetpoints');
  216. let selects = wrapper.querySelector('#fatcat_video_vip_select');
  217. let aTag = wrapper.querySelector('#fatcat_video_vip_link');
  218. function updateSelectView(accessPoints, curAccessPoint) {
  219. selects.innerHTML = accessPoints.map(({ name, url }) => `<option value="${url}" ${name == curAccessPoint.name ? 'selected' : ''}>${name}</option>`).join();
  220. }
  221. async function applyAccessPoint(accessPoints, curAccessPoint) {
  222. await setUserConfig('accessPoints', accessPoints);
  223. await setUserConfig('curAccessPoint', curAccessPoint);
  224. updateSelectView(accessPoints, curAccessPoint);
  225. updateView(curAccessPoint);
  226. displayResult('✅ 完成');
  227. return;
  228. }
  229. let resultToasting;
  230. function displayResult(html) {
  231. let rawTitle = '当前解析点 <span class="fatcat_video_vip_link">➜</span>';
  232. popoverTitle.innerHTML = html;
  233. clearTimeout(resultToasting);
  234. resultToasting = setTimeout(() => popoverTitle.innerHTML = rawTitle, 1500);
  235. }
  236. addBtn.onclick = async () => {
  237. if (!(nameInput.value.trim() && urlInput.value.trim())) {
  238. displayResult('❌ 请完整输入');
  239. return;
  240. }
  241. let accessPoints = await getUserConfig('accessPoints');
  242. let sameUrlPoint = accessPoints.find(({ url }) => url == urlInput.value.trim());
  243. if (sameUrlPoint) {
  244. nameInput.value = sameUrlPoint.name;
  245. displayResult('❌ 重复数据');
  246. return;
  247. }
  248. let newPoint = { name: nameInput.value.trim(), url: urlInput.value.trim() };
  249. let newAccessPoints = accessPoints.concat([newPoint]);
  250. applyAccessPoint(newAccessPoints, newPoint);
  251. }
  252. delBtn.onclick = async () => {
  253. let accessPoints = await getUserConfig('accessPoints');
  254. let curAccessPoint = await getUserConfig('curAccessPoint');
  255. if (accessPoints.length == 0) {
  256. displayResult('❌ 数据已被清空');
  257. return;
  258. }
  259. nameInput.value = curAccessPoint.name;
  260. urlInput.value = curAccessPoint.url;
  261. let curAccessPointIndex = accessPoints.findIndex(point => point.name == curAccessPoint.name);
  262. let newAccessPoints = accessPoints.filter(point => point.name != curAccessPoint.name);
  263. curAccessPoint = newAccessPoints[Math.min(curAccessPointIndex, newAccessPoints.length - 1)];
  264. applyAccessPoint(newAccessPoints, curAccessPoint);
  265. }
  266. resetBtn.onclick = async () => {
  267. applyAccessPoint(ACCESS_POINT, ACCESS_POINT[0]);
  268. }
  269. function setWrapper(e) {
  270. if (e.target.matches('#fatcat_video_vip,#fatcat_video_vip *')) return;
  271. e.target.classList.toggle('fatcat_video_vip_custom_wrapper', true);
  272. window.removeEventListener('mousedown', setWrapper);
  273. document.querySelector('#fatcat_video_vip_select_area_css').remove();
  274. e.stopPropagation();
  275. e.preventDefault();
  276. }
  277. setWrapperBtn.addEventListener('click', () => {
  278. try {
  279. document.querySelector('#fatcat_video_vip_select_area_css').remove();
  280. document.querySelectorAll('.fatcat_video_vip_custom_wrapper').forEach(element => {
  281. element.classList.toggle('fatcat_video_vip_custom_wrapper', false);
  282. });
  283. } catch (e) { }
  284. searchSelector = ['.fatcat_video_vip_custom_wrapper'];
  285. let selectAreaCss = document.createElement('style');
  286. selectAreaCss.id = 'fatcat_video_vip_select_area_css';
  287. selectAreaCss.innerHTML = `
  288. *:hover{outline: 2px solid #13bf92!important;outline-offset: -2px!important;box-shadow: inset 0 0 20px 7px #13bf9db3;z-index: 99999999;}
  289. *:has(*:hover){outline: unset!important;box-shadow: unset!important;}
  290. :is(#fatcat_video_vip,#fatcat_video_vip *):hover{outline:unset!important;box-shadow: unset!important;}`;
  291. document.body.append(selectAreaCss);
  292. setWrapperBtn.innerHTML = '请点击需要插入视频的区域';
  293. setTimeout(() => setWrapperBtn.innerHTML = '若未生效,可能区域过小📍点击重选', 3000);
  294. window.addEventListener('mousedown', setWrapper);
  295. })
  296. function updateView(curAccessPoint) {
  297. let iframe = document.querySelector('#fatcat_video_vip_iframe');
  298. if (!iframe) return true;
  299. let src = curAccessPoint ? curAccessPoint.url + window.location.href : '';
  300. aTag.href = src;
  301. if (iframe.src != src) {
  302. iframe.src = src;
  303. return true;
  304. }
  305. }
  306. window.addEventListener('mousedown', async (e) => {
  307. if (!(e.target.matches("#fatcat_video_vip *") || e.target.matches("#fatcat_video_vip_iframe"))) {
  308. let curAccessPoint = await getUserConfig('curAccessPoint');
  309. useInterval(() => updateView(curAccessPoint), 100, 10000);
  310. }
  311. });
  312. selects.onchange = async () => {
  313. let accessPoints = await getUserConfig('accessPoints');
  314. let curAccessPoint = accessPoints.find(({ url }) => url == selects.value)
  315. await setUserConfig('curAccessPoint', curAccessPoint);
  316. updateView(curAccessPoint);
  317. };
  318. document.body.appendChild(wrapper);
  319. }
  320. function checkUrlValid(url) {
  321. return !(/http(s?):\/\/.*http(s?):\/\/.*/.test(url) || /^http(s?):\/\/[^\/]*\/$/.test(url));
  322. }
  323. async function waitForPageLoad() {
  324. return new Promise(resolve => { if (document.readyState == 'complete') resolve(); else document.addEventListener('DOMContentLoaded', resolve); });
  325. }
  326. function stopVideo() { document.querySelectorAll('video').forEach(video => { video.onpause = null; video.play = () => { }; video.pause(); video.muted = true; }) }
  327. async function inject() {
  328. if (!checkUrlValid(window.location.href)) return;
  329. patches[window.location.host] && patches[window.location.host]();
  330. await waitForPageLoad();
  331. let accessPoints = await getUserConfig('accessPoints');
  332. if (!accessPoints) {
  333. await setUserConfig('accessPoints', ACCESS_POINT);
  334. await setUserConfig('curAccessPoint', ACCESS_POINT[0]);
  335. accessPoints = ACCESS_POINT;
  336. }
  337. let curAccessPoint = await getUserConfig('curAccessPoint');
  338. createMenu(accessPoints, curAccessPoint);
  339. let videoWrapper = await findVideoWrapper();
  340. try {
  341. stopVideo(); useInterval(stopVideo, 2000, Infinity);
  342. document.querySelector('#fatcat_video_vip_setwrapper').style.display = 'none';
  343. } catch (e) { }
  344. createVideoFrame(videoWrapper, curAccessPoint);
  345. }
  346. inject();
  347. })();