使用 MPV 播放

通过 mpv-handler 播放网页上的视频和歌曲

目前为 2024-05-11 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Play with MPV
  3. // @name:en-US Play with MPV
  4. // @name:zh-CN 使用 MPV 播放
  5. // @name:zh-TW 使用 MPV 播放
  6. // @description Play videos and songs on the website via mpv-handler
  7. // @description:en-US Play videos and songs on the website via mpv-handler
  8. // @description:zh-CN 通过 mpv-handler 播放网页上的视频和歌曲
  9. // @description:zh-TW 通過 mpv-handler 播放網頁上的視頻和歌曲
  10. // @namespace play-with-mpv-handler
  11. // @version 2024.05.11
  12. // @author Akatsuki Rui
  13. // @license MIT License
  14. // @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@06f2015c04db3aaab9717298394ca4f025802873/gm_config.js
  15. // @grant GM_info
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // @grant GM_notification
  19. // @run-at document-idle
  20. // @noframes
  21. // @match *://*.youtube.com/*
  22. // @match *://*.twitch.tv/*
  23. // @match *://*.crunchyroll.com/*
  24. // @match *://*.bilibili.com/*
  25. // @match *://*.kick.com/*
  26. // @match *://*.pornhub.com/*
  27. // ==/UserScript==
  28.  
  29. "use strict";
  30.  
  31. const MPV_HANDLER_VERSION = "v0.3.8";
  32.  
  33. const MATCHERS = {
  34. "www.youtube.com": /www.youtube.com\/(watch|playlist|shorts)\?/gi,
  35. "m.youtube.com": /m.youtube.com\/(watch|playlist|shorts)\?/gi,
  36. "www.twitch.tv":
  37. /www.twitch.tv\/(?!(directory|downloads|jobs|p|turbo)\/).+/gi,
  38. "clips.twitch.tv": /clips.twitch.tv/gi,
  39. "www.crunchyroll.com": /www.crunchyroll.com\/.*watch\/([0-9]|[A-Z])+/gi,
  40. "beta.crunchyroll.com": /beta.crunchyroll.com\/.*watch\/([0-9]|[A-Z])+/gi,
  41. "live.bilibili.com": /live.bilibili.com\/[0-9]+/gi,
  42. "www.bilibili.com": /www.bilibili.com\/video\/(av|bv)/gi,
  43. "kick.com": /kick.com\/(?!(categories)\/).+/gi,
  44. "www.pornhub.com": /www.pornhub.com\/view_video\.php/gi,
  45. };
  46.  
  47. const ICON_MPV =
  48. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0\
  49. PSI2NCIgdmVyc2lvbj0iMSI+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4yIiBjeD0iMzIiIGN5\
  50. PSIzMyIgcj0iMjgiLz4KIDxjaXJjbGUgc3R5bGU9ImZpbGw6IzhkMzQ4ZSIgY3g9IjMyIiBjeT0i\
  51. MzIiIHI9IjI4Ii8+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4zIiBjeD0iMzQuNSIgY3k9IjI5\
  52. LjUiIHI9IjIwLjUiLz4KIDxjaXJjbGUgc3R5bGU9Im9wYWNpdHk6LjIiIGN4PSIzMiIgY3k9IjMz\
  53. IiByPSIxNCIvPgogPGNpcmNsZSBzdHlsZT0iZmlsbDojZmZmZmZmIiBjeD0iMzIiIGN5PSIzMiIg\
  54. cj0iMTQiLz4KIDxwYXRoIHN0eWxlPSJmaWxsOiM2OTFmNjkiIHRyYW5zZm9ybT0ibWF0cml4KDEu\
  55. NTE1NTQ0NSwwLDAsMS41LC0zLjY1Mzg3OSwtNC45ODczODQ4KSIgZD0ibTI3LjE1NDUxNyAyNC42\
  56. NTgyNTctMy40NjQxMDEgMi0zLjQ2NDEwMiAxLjk5OTk5OXYtNC0zLjk5OTk5OWwzLjQ2NDEwMiAy\
  57. eiIvPgogPHBhdGggc3R5bGU9ImZpbGw6I2ZmZmZmZjtvcGFjaXR5Oi4xIiBkPSJNIDMyIDQgQSAy\
  58. OCAyOCAwIDAgMCA0IDMyIEEgMjggMjggMCAwIDAgNC4wMjE0ODQ0IDMyLjU4NTkzOCBBIDI4IDI4\
  59. IDAgMCAxIDMyIDUgQSAyOCAyOCAwIDAgMSA1OS45Nzg1MTYgMzIuNDE0MDYyIEEgMjggMjggMCAw\
  60. IDAgNjAgMzIgQSAyOCAyOCAwIDAgMCAzMiA0IHoiLz4KPC9zdmc+Cg==";
  61.  
  62. const ICON_SETTINGS =
  63. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0\
  64. PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KIDxkZWZzPgogIDxzdHlsZSBpZD0iY3VycmVudC1j\
  65. b2xvci1zY2hlbWUiIHR5cGU9InRleHQvY3NzIj4KICAgLkNvbG9yU2NoZW1lLVRleHQgeyBjb2xv\
  66. cjojNDQ0NDQ0OyB9IC5Db2xvclNjaGVtZS1IaWdobGlnaHQgeyBjb2xvcjojNDI4NWY0OyB9CiAg\
  67. PC9zdHlsZT4KIDwvZGVmcz4KIDxwYXRoIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvciIgY2xhc3M9\
  68. IkNvbG9yU2NoZW1lLVRleHQiIGQ9Ik0gNi4yNSAxIEwgNi4wOTU3MDMxIDIuODQzNzUgQSA1LjUg\
  69. NS41IDAgMCAwIDQuNDg4MjgxMiAzLjc3MzQzNzUgTCAyLjgxMjUgMi45ODQzNzUgTCAxLjA2MjUg\
  70. Ni4wMTU2MjUgTCAyLjU4Mzk4NDQgNy4wNzIyNjU2IEEgNS41IDUuNSAwIDAgMCAyLjUgOCBBIDUu\
  71. NSA1LjUgMCAwIDAgMi41ODAwNzgxIDguOTMxNjQwNiBMIDEuMDYyNSA5Ljk4NDM3NSBMIDIuODEy\
  72. NSAxMy4wMTU2MjUgTCA0LjQ4NDM3NSAxMi4yMjg1MTYgQSA1LjUgNS41IDAgMCAwIDYuMDk1NzAz\
  73. MSAxMy4xNTIzNDQgTCA2LjI0NjA5MzggMTUuMDAxOTUzIEwgOS43NDYwOTM4IDE1LjAwMTk1MyBM\
  74. IDkuOTAwMzkwNiAxMy4xNTgyMDMgQSA1LjUgNS41IDAgMCAwIDExLjUwNzgxMiAxMi4yMjg1MTYg\
  75. TCAxMy4xODM1OTQgMTMuMDE3NTc4IEwgMTQuOTMzNTk0IDkuOTg2MzI4MSBMIDEzLjQxMjEwOSA4\
  76. LjkyOTY4NzUgQSA1LjUgNS41IDAgMCAwIDEzLjQ5NjA5NCA4LjAwMTk1MzEgQSA1LjUgNS41IDAg\
  77. MCAwIDEzLjQxNjAxNiA3LjA3MDMxMjUgTCAxNC45MzM1OTQgNi4wMTc1NzgxIEwgMTMuMTgzNTk0\
  78. IDIuOTg2MzI4MSBMIDExLjUxMTcxOSAzLjc3MzQzNzUgQSA1LjUgNS41IDAgMCAwIDkuOTAwMzkw\
  79. NiAyLjg0OTYwOTQgTCA5Ljc1IDEgTCA2LjI1IDEgeiBNIDggNiBBIDIgMiAwIDAgMSAxMCA4IEEg\
  80. MiAyIDAgMCAxIDggMTAgQSAyIDIgMCAwIDEgNiA4IEEgMiAyIDAgMCAxIDggNiB6IiB0cmFuc2Zv\
  81. cm09InRyYW5zbGF0ZSg0IDQpIi8+Cjwvc3ZnPgo=";
  82.  
  83. const MPV_CSS = `
  84. .pwm-play {
  85. width: 48px;
  86. height: 48px;
  87. border: 0;
  88. border-radius: 50%;
  89. background-size: 48px;
  90. background-image: url(data:image/svg+xml;base64,${ICON_MPV});
  91. background-repeat: no-repeat;
  92. }
  93. .pwm-settings {
  94. opacity: 0;
  95. visibility: hidden;
  96. transition: all 0.2s ease-in-out;
  97. display: block;
  98. position: absolute;
  99. top: -32px;
  100. width: 32px;
  101. height: 32px;
  102. margin-left: 8px;
  103. border: 0;
  104. border-radius: 50%;
  105. background-size: 32px;
  106. background-color: #eeeeee;
  107. background-image: url(data:image/svg+xml;base64,${ICON_SETTINGS});
  108. background-repeat: no-repeat;
  109. }
  110. .play-with-mpv {
  111. z-index: 99999;
  112. position: fixed;
  113. left: 8px;
  114. bottom: 8px;
  115. }
  116. .pwm-play:hover + .pwm-settings,
  117. .pwm-settings:hover {
  118. opacity: 1;
  119. visibility: visible;
  120. transition: all 0.2s ease-in-out;
  121. }
  122. `;
  123.  
  124. const CONFIG_ID = "play-with-mpv";
  125.  
  126. const CONFIG_CSS = `
  127. body {
  128. display: flex;
  129. justify-content: center;
  130. }
  131. #${CONFIG_ID}_wrapper {
  132. display: flex;
  133. flex-direction: column;
  134. justify-content: center;
  135. }
  136. #${CONFIG_ID} .config_header {
  137. display: flex;
  138. align-items: center;
  139. padding: 12px;
  140. }
  141. #${CONFIG_ID} .config_var {
  142. margin: 0 0 12px 0;
  143. }
  144. #${CONFIG_ID} .field_label {
  145. display: inline-block;
  146. width: 140px;
  147. font-size: 14px;
  148. }
  149. #${CONFIG_ID}_field_cookies,
  150. #${CONFIG_ID}_field_profile,
  151. #${CONFIG_ID}_field_quality,
  152. #${CONFIG_ID}_field_v_codec,
  153. #${CONFIG_ID}_field_console {
  154. width: 80px;
  155. height: 24px;
  156. font-size: 14px;
  157. text-align: center;
  158. }
  159. #${CONFIG_ID}_buttons_holder {
  160. display: flex;
  161. flex-direction: column;
  162. }
  163. #${CONFIG_ID} .saveclose_buttons {
  164. margin: 1px;
  165. padding: 4px 0;
  166. }
  167. #${CONFIG_ID} .reset_holder {
  168. padding-top: 4px;
  169. }
  170. `;
  171.  
  172. const CONFIG_IFRAME_CSS = `
  173. position: fixed;
  174. z-index: 99999;
  175. width: 300px;
  176. height: 400px;
  177. border: 1px solid;
  178. border-radius: 10px;
  179. `;
  180.  
  181. const CONFIG_FIELDS = {
  182. cookies: {
  183. label: "Try Pass Cookies",
  184. type: "select",
  185. options: ["yes", "no"],
  186. default: "no",
  187. },
  188. profile: {
  189. label: "MPV Profile",
  190. type: "text",
  191. default: "default",
  192. },
  193. quality: {
  194. label: "Prefer Video Quality",
  195. type: "select",
  196. options: ["default", "2160p", "1440p", "1080p", "720p", "480p", "360p"],
  197. default: "default",
  198. },
  199. v_codec: {
  200. label: "Prefer Video Codec",
  201. type: "select",
  202. options: ["default", "av01", "vp9", "h265", "h264"],
  203. default: "default",
  204. },
  205. console: {
  206. label: "Run With Console",
  207. type: "select",
  208. options: ["yes", "no"],
  209. default: "yes",
  210. },
  211. };
  212.  
  213. // GM_config init
  214. GM_config.init({
  215. id: CONFIG_ID,
  216. title: GM_info.script.name,
  217. fields: CONFIG_FIELDS,
  218. events: {
  219. init: () => {
  220. let quality = GM_config.get("quality").toLowerCase();
  221. let v_codec = GM_config.get("v_codec").toLowerCase();
  222.  
  223. if (!CONFIG_FIELDS.quality.options.includes(quality)) {
  224. GM_config.set("quality", "default");
  225. }
  226. if (!CONFIG_FIELDS.v_codec.options.includes(v_codec)) {
  227. GM_config.set("v_codec", "default");
  228. }
  229. },
  230. save: () => {
  231. let profile = GM_config.get("profile").trim();
  232.  
  233. if (profile === "") {
  234. GM_config.set("profile", "default");
  235. } else {
  236. GM_config.set("profile", profile);
  237. }
  238.  
  239. updateButton(location.href);
  240. GM_config.close();
  241. },
  242. reset: () => {
  243. GM_config.save();
  244. },
  245. },
  246. css: CONFIG_CSS.trim(),
  247. });
  248.  
  249. // URL-safe base64 encode
  250. function btoaUrl(url) {
  251. return btoa(url).replace(/\//g, "_").replace(/\+/g, "-").replace(/\=/g, "");
  252. }
  253.  
  254. // Generate protocol
  255. function generateProto(url) {
  256. let cookies = GM_config.get("cookies").toLowerCase();
  257. let profile = GM_config.get("profile").trim();
  258. let quality = GM_config.get("quality").toLowerCase();
  259. let v_codec = GM_config.get("v_codec").toLowerCase();
  260. let console = GM_config.get("console").toLowerCase();
  261. let options = [];
  262.  
  263. let proto;
  264.  
  265. if (console === "yes") {
  266. proto = "mpv-debug://play/" + btoaUrl(url);
  267. } else {
  268. proto = "mpv://play/" + btoaUrl(url);
  269. }
  270. if (cookies === "yes") {
  271. options.push("cookies=" + document.location.hostname + ".txt");
  272. }
  273. if (profile !== "default" && profile !== "") {
  274. options.push("profile=" + profile);
  275. }
  276. if (quality !== "default") {
  277. options.push("quality=" + quality);
  278. }
  279. if (v_codec !== "default") {
  280. options.push("v_codec=" + v_codec);
  281. }
  282.  
  283. if (options.length !== 0) {
  284. proto += "/?";
  285.  
  286. options.forEach((option, index) => {
  287. proto += option;
  288.  
  289. if (index + 1 !== options.length) {
  290. proto += "&";
  291. }
  292. });
  293. }
  294.  
  295. return proto;
  296. }
  297.  
  298. // Check the URL is matched or not
  299. function matchUrl(url) {
  300. if (MATCHERS[location.hostname]) {
  301. return url.search(MATCHERS[location.hostname]) !== -1;
  302. } else {
  303. return false;
  304. }
  305. }
  306.  
  307. // Update button display status and URL
  308. function updateButton(url) {
  309. let isMatch = matchUrl(url);
  310. let button = document.getElementsByClassName("pwm-play")[0];
  311.  
  312. if (button) {
  313. button.style =
  314. isMatch && !document.fullscreenElement
  315. ? "display: block"
  316. : "display: none";
  317. button.href = isMatch ? generateProto(url) : "";
  318. }
  319. }
  320.  
  321. // Notify update about mpv-handler
  322. function notifyUpdate() {
  323. let version = GM_getValue("mpvHandlerVersion", null);
  324.  
  325. if (version !== MPV_HANDLER_VERSION) {
  326. const UPDATE_NOTIFY = {
  327. title: `${GM_info.script.name}`,
  328. text: `mpv-handler is upgraded to ${MPV_HANDLER_VERSION}\n\nClick to check updates`,
  329. onclick: () => {
  330. window.open("https://github.com/akiirui/mpv-handler/releases/latest");
  331. },
  332. };
  333.  
  334. GM_notification(UPDATE_NOTIFY);
  335. GM_setValue("mpvHandlerVersion", MPV_HANDLER_VERSION);
  336. }
  337. }
  338.  
  339. // Add play and settings buttons to page
  340. function createButton() {
  341. let head = document.getElementsByTagName("head")[0];
  342. let style = document.createElement("style");
  343.  
  344. if (head) {
  345. style.innerHTML = MPV_CSS.trim();
  346. head.appendChild(style);
  347. }
  348.  
  349. let body = document.body;
  350. let buttonDiv = document.createElement("div");
  351. let buttonPlay = document.createElement("a");
  352. let buttonSettings = document.createElement("button");
  353.  
  354. let pauseVideo = (e) => {
  355. let videoElement = document.getElementsByTagName("video")[0];
  356. if (videoElement) {
  357. videoElement.pause();
  358. } else {
  359. setTimeout(pauseVideo, 500, e);
  360. }
  361. if (e.stopPropagation) e.stopPropagation();
  362. };
  363.  
  364. if (body) {
  365. buttonPlay.className = "pwm-play";
  366. buttonPlay.style = "display: none";
  367. buttonPlay.target = "_blank";
  368. buttonPlay.addEventListener("click", pauseVideo);
  369.  
  370. buttonSettings.className = "pwm-settings";
  371. buttonSettings.addEventListener("click", () => {
  372. if (!GM_config.isOpen) {
  373. GM_config.open();
  374. GM_config.frame.style = CONFIG_IFRAME_CSS.trim();
  375. }
  376. });
  377.  
  378. buttonDiv.className = "play-with-mpv";
  379. buttonDiv.appendChild(buttonPlay);
  380. buttonDiv.appendChild(buttonSettings);
  381.  
  382. body.appendChild(buttonDiv);
  383.  
  384. document.addEventListener("fullscreenchange", () => {
  385. let button = document.getElementsByClassName("pwm-play")[0];
  386.  
  387. button.style = document.fullscreenElement
  388. ? "display: none"
  389. : "display: block";
  390. });
  391. }
  392. }
  393.  
  394. // Detect PJAX changes
  395. function detectPJAX() {
  396. let previousUrl = null;
  397. let currentUrl = null;
  398.  
  399. setInterval(() => {
  400. currentUrl = location.href;
  401.  
  402. if (previousUrl !== currentUrl) {
  403. updateButton(currentUrl);
  404. previousUrl = currentUrl;
  405. }
  406. }, 500);
  407. }
  408.  
  409. notifyUpdate();
  410. createButton();
  411. detectPJAX();

QingJ © 2025

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