A2P

Anime2Potplayer,用Potplayer打开浏览器播放的动漫,然后本地使用SVP4补帧!Potplayer需要是安装版,否则不生效。

  1. // ==UserScript==
  2. // @name A2P
  3. // @namespace https://github.com/MakotoArai-CN/A2P
  4. // @version 1.1.2
  5. // @description Anime2Potplayer,用Potplayer打开浏览器播放的动漫,然后本地使用SVP4补帧!Potplayer需要是安装版,否则不生效。
  6. // @author MakotoArai(https://github.com/MakotoArai-CN)
  7. // @supportURL https://blog.ciy.cool
  8. // @license GPL-v3
  9. // @icon https://cravatar.cn/avatar/1e84fce3269537e4aa7473602516bf6d?s=256
  10. // @match *anich.emmmm.eu.org/*
  11. // @match *.mutedm.com/*
  12. // @match *.iyinghua.com/*
  13. // @match *.5dm.link/*
  14. // @match *.dmd77.com/*
  15. // @match *.agefans.la/*
  16. // @match *43.240.156.118:8443/*
  17. // @match *tinaacg.net/*
  18. // @match *susudyy.com/*
  19. // @match *.5o5k.com/*
  20. // @match *.k6dm.com/*
  21. // @match *.233dm.com/*
  22. // @grant GM_setValue
  23. // @grant GM_getValue
  24. // @grant unsafeWindow
  25. // @grant GM_xmlhttpRequest
  26. // @grant GM_notification
  27. // @grant GM_addStyle
  28. // @grant GM_setClipboard
  29. // ==/UserScript==
  30.  
  31. 'use strict';
  32.  
  33. const m3u8Urls = new Set();
  34. let resultsShown = false;
  35.  
  36. function showResults(title, text, timeout) {
  37. // 发送桌面通知
  38. if (typeof GM_notification !== 'undefined') {
  39. GM_notification({
  40. title: title,
  41. text: text,
  42. timeout: timeout,
  43. //点击后触发复制
  44. onclick: function () {
  45. GM_setClipboard(text);
  46. }
  47. });
  48. }
  49. }
  50.  
  51. // 拦截所有网络请求
  52. if (typeof GM_xmlhttpRequest !== 'undefined') {
  53. const originalXHR = unsafeWindow.XMLHttpRequest;
  54. unsafeWindow.XMLHttpRequest = function () {
  55. const xhr = new originalXHR();
  56. const originalOpen = xhr.open;
  57.  
  58. xhr.open = function (method, url) {
  59. if (url && /\.m3u8($|\?)/i.test(url)) {
  60. m3u8Urls.add(url);
  61. if (GM_getValue("notify")) showResults("M3U8嗅探到:", url, 5000);
  62. console.log('拦截到M3U8 (XHR):', url);
  63. GM_setValue("Reallyurl", url);
  64. }
  65. return originalOpen.apply(this, arguments);
  66. };
  67.  
  68. return xhr;
  69. };
  70. }
  71.  
  72. // 监听动态创建的video元素
  73. new MutationObserver(function (mutations) {
  74. mutations.forEach(function (mutation) {
  75. mutation.addedNodes.forEach(function (node) {
  76. if (node.nodeName === 'VIDEO') checkVideoElement(node);
  77. if (node.querySelectorAll) node.querySelectorAll('video').forEach(checkVideoElement);
  78. });
  79. });
  80. }).observe(document, { childList: true, subtree: true });
  81.  
  82. /**
  83. * .m3u8嗅探
  84. * @param {*} video
  85. */
  86. function checkVideoElement(video) {
  87. if (video.src && /\.m3u8($|\?)/i.test(video.src)) {
  88. m3u8Urls.add(video.src);
  89. // console.log('发现M3U8 video元素:', video.src);
  90. // showResults();
  91. }
  92. }
  93. // 初始检查
  94. document.querySelectorAll('video').forEach(checkVideoElement);
  95. // console.log('M3U8综合嗅探已激活');
  96.  
  97. window.onload = function () {
  98. console.info("%cA2P%c%s", "color:red;font-size:40px;font-weight:bold;", "color:black;font-size:16px;font-weight:normal", "\n" + GM_info.script.version);
  99. // 定时器用于动态嗅探视频链接
  100. const videoTimer = setInterval(findVideoUrl, 1000);
  101.  
  102. // 域名包含 anich.emmmm.eu.org 则启用下面的逻辑
  103. if (window.location.href.includes("anich.emmmm.eu.org")) {
  104. // 定时器检测url是否改变,如果改变则重新调用findVideoUrl
  105. setInterval(function () {
  106. if (GM_getValue("url") !== window.location.href) {
  107. // 存入url方便对比
  108. GM_setValue("url", window.location.href);
  109. findVideoUrl();
  110. }
  111. }, 1500);
  112. }
  113.  
  114. function Launch(App, url) {
  115. try {
  116. window.location.href = `${App}://${url}`;
  117. console.log("Launch to:" + url);
  118.  
  119. } catch (error) {
  120. alert(`请先安装 ${App}`);
  121. }
  122. }
  123.  
  124. function findVideoUrl() {
  125. const videoElement = document.querySelector("video");
  126. if (videoElement && videoElement.src) {
  127. clearInterval(videoTimer);
  128. preparePotplayerInteraction(videoElement, GM_getValue("check") ?? false);
  129. }
  130. }
  131.  
  132. function preparePotplayerInteraction(videoElement, check = true) {
  133. let videoUrl = videoElement.src;
  134. console.log(`检测到视频链接: ${videoUrl}`);
  135. if (videoElement.src.includes("blob:")) videoUrl = GM_getValue("Reallyurl");;
  136. creatBtn(videoElement);
  137. if (check) {
  138. Launch("potplayer", videoUrl)
  139. // 检测是否播放,如果正在播放则暂停网页的播放
  140. var pause_Flag = 0;
  141. const checkTimer = setInterval(() => {
  142. const isPlaying = !videoElement.paused && !videoElement.ended && videoElement.readyState > 2;
  143. // console.log(isPlaying ? "正在播放" : "已暂停或结束");
  144. if (isPlaying) videoElement.pause();
  145. if (!isPlaying || pause_Flag > 100) clearInterval(checkTimer);
  146. pause_Flag++;
  147. }, 1500);
  148. };
  149.  
  150. }
  151.  
  152. function creatBtn(videoElement) {
  153. // 插入自定义CDN
  154. document.head.insertAdjacentHTML("beforeend", `
  155. <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
  156. `);
  157. // 右键菜单
  158. var menu = document.createElement("div");
  159. document.head.insertAdjacentHTML("beforeend", `
  160. <style>
  161. a {text-decoration: none;}
  162. div.usercm{background-repeat:no-repeat;background-position:center center;background-size:cover;background-color:#fff;font-size:13px!important;width:160px;-moz-box-shadow:1px 1px 3px rgba
  163. (0,0,0,.3);box-shadow:0px 0px 15px #333;position:absolute;display:none;z-index:10000;opacity:0.9; border-radius: 8px;}
  164. div.usercm ul{list-style-type:none;list-style-position:outside;margin:0px;padding:0px;display:block}
  165. div.usercm ul li{margin:0px;padding:0px;line-height:35px;}
  166. div.usercm ul li a{color:#666;padding:0 15px;display:block}
  167. /* div.usercm ul li a:hover{color:#fff;background:rgba(170,222,18,0.88)} */
  168. div.usercm ul li a:hover{color:#fff;background:rgba(15, 120, 233, 0.88)} /* 蓝色 */
  169. div.usercm ul li a i{margin-right:10px}
  170. a.disabled{color:#c8c8c8!important;cursor:not-allowed}
  171. a.disabled:hover{background-color:rgba(255,11,11,0)!important}
  172. div.usercm{background:#fff !important;}
  173. </style>
  174. )`);
  175. menu.innerHTML = `
  176. <div class="usercm" style="left: 199px; top: 5px; display: none;">
  177. <ul>
  178. <li style="border-bottom:1px solid gray"><a href="javascript:window.location.reload();"><i class="fa fa-refresh fa-fw"></i><span>重载网页</span></a></li>
  179. <li><a href="javascript:void(0);" class="potplayer"><i class="fas fa-external-link-alt"></i><span>Potplayer(X)</span></a></li>
  180. <li><a href="javascript:void(0);" class="aa2p"><i class="fas fa-robot"></i><span>自动跳转</span></a></li>
  181. <li><a href="javascript:void(0);" class="notify"><i class="far fa-bell-slash"></i><span>关闭通知</span></a></li>
  182. <li><a href="https://blog.ciy.cool"><i class="fas fa-blog"></i><span>联系作者</span></a></li>
  183. </ul>
  184. </div>
  185. `;
  186. document.body.appendChild(menu);
  187.  
  188. function menuFun(GMValue, element, icon_on, icon_off, text) {
  189. const check = GM_getValue(GMValue) ?? false;
  190. if (check) {
  191. GM_setValue(GMValue, false);
  192. element.innerHTML = `<i class="${icon_off}"></i><span>开启${text}</span>`;
  193. } else {
  194. GM_setValue(GMValue, true);
  195. element.innerHTML = `<i class="${icon_on}"></i><span>关闭${text}</span>`;
  196. }
  197. }
  198.  
  199.  
  200.  
  201. // 自定义鼠标右键菜单行为
  202. (function () {
  203. let mouseX = 0;
  204. let mouseY = 0;
  205. let windowWidth = 0;
  206. let windowHeight = 0;
  207.  
  208. // 获取元素
  209. const menu = document.querySelector('.usercm');
  210.  
  211. // 鼠标移动事件
  212. window.addEventListener('mousemove', function (e) {
  213. windowWidth = window.innerWidth;
  214. windowHeight = window.innerHeight;
  215. mouseX = e.clientX;
  216. mouseY = e.clientY;
  217.  
  218. // 设置菜单位置
  219. let left = e.pageX;
  220. let top = e.pageY;
  221.  
  222. if (mouseX + menu.offsetWidth >= windowWidth) left = left - menu.offsetWidth - 5;
  223. if (mouseY + menu.offsetHeight >= windowHeight) top = top - menu.offsetHeight - 5;
  224.  
  225. // 绑定右键点击事件
  226. document.documentElement.addEventListener('contextmenu', function (event) {
  227. if (event.button === 2) { // 右键点击
  228. event.preventDefault();
  229. menu.style.left = `${left}px`;
  230. menu.style.top = `${top}px`;
  231. menu.style.display = 'block';
  232. }
  233. });
  234.  
  235. // 点击隐藏菜单
  236. document.documentElement.addEventListener('click', function () {
  237. menu.style.display = 'none';
  238. });
  239. });
  240.  
  241. // 禁用默认右键菜单
  242. window.oncontextmenu = function (e) {
  243. e.preventDefault();
  244. return false;
  245. };
  246.  
  247. // 判断是否是移动端
  248. const userAgent = navigator.userAgent;
  249. const mobileKeywords = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod'];
  250. let isMobile = false;
  251.  
  252. for (let keyword of mobileKeywords) {
  253. if (userAgent.indexOf(keyword) > -1) {
  254. isMobile = true;
  255. break;
  256. }
  257. }
  258.  
  259. })();
  260. const potplayer = document.querySelector(".potplayer");
  261. const aa2p = document.querySelector(".aa2p");
  262. const notify = document.querySelector(".notify");
  263. let videoUrl = videoElement.src;
  264. if (videoElement.src.includes("blob:")) videoUrl = GM_getValue("Reallyurl");;
  265. potplayer.addEventListener("click", function () {
  266. Launch("potplayer", videoUrl);
  267. videoElement.pause();
  268. })
  269.  
  270. document.onkeydown = function (e) {
  271. const keyNum = window.event ? e.keyCode : e.which;
  272. if (e.altKey && Number.isInteger(keyNum)) {
  273. switch (keyNum) {
  274. case 88:// X 键--> potplayer
  275. console.log("potplayer://" + videoUrl);
  276. Launch("potplayer", videoUrl)
  277. videoElement.pause();
  278. break;
  279. case 90:// Z 键--> 自动跳转
  280. console.log("%cAuto jump %c%s", "", GM_getValue("check") ? "color:green;font-weight:bold;" : "color:red;font-weight:bold;", + GM_getValue("check") ? "Turn on" : "Turn off");
  281. menuFun("check", this, "fas fa-toggle-on", "fas fa-toggle-off", "自动跳转");
  282. break;
  283. }
  284. }
  285. };
  286.  
  287. aa2p.innerHTML = `<i class="${GM_getValue("check") ? "fas fa-toggle-on" : "fas fa-toggle-off"}"></i><span>${GM_getValue("check") ? "关闭自动跳转" : "开启自动跳转"}</span>`;
  288. aa2p.addEventListener("click", function () { menuFun("check", this, "fas fa-toggle-on", "fas fa-toggle-off", "自动跳转"); });
  289.  
  290. notify.innerHTML = `<i class="${GM_getValue("notify") ? "fas fa-bell" : "far fa-bell-slash"}"}"></i><span>${GM_getValue("notify") ? "关闭通知" : "开启通知"}</span>`;
  291. notify.addEventListener("click", function () { menuFun("notify", this, "fas fa-bell", "far fa-bell-slash", "通知"); });
  292. }
  293. }