使用 MPV 播放

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

当前为 2023-01-18 提交的版本,查看 最新版本

  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 2023.01.18
  12. // @author Akatsuki Rui
  13. // @license MIT License
  14. // @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@a4a49b47ecfb1d8fcd27049cc0e8114d05522a0f/gm_config.js
  15. // @grant GM_info
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // @grant GM_notification
  19. // @grant GM_openInTab
  20. // @run-at document-idle
  21. // @noframes
  22. // @match *://www.youtube.com/*
  23. // @match *://m.youtube.com/*
  24. // @match *://www.twitch.tv/*
  25. // @match *://clips.twitch.tv/*
  26. // @match *://www.crunchyroll.com/*
  27. // @match *://beta.crunchyroll.com/*
  28. // @match *://www.bilibili.com/video/*
  29. // @match *://live.bilibili.com/*
  30. // ==/UserScript==
  31.  
  32. "use strict";
  33.  
  34. const MPV_HANDLER_VERSION = "v0.3.2";
  35.  
  36. const MATCHERS = {
  37. "www.youtube.com": /www.youtube.com\/(watch|playlist|shorts)\?/gi,
  38. "m.youtube.com": /m.youtube.com\/(watch|playlist|shorts)\?/gi,
  39. "www.twitch.tv":
  40. /www.twitch.tv\/(?!(directory|downloads|jobs|p|turbo)\/).+/gi,
  41. "clips.twitch.tv": /clips.twitch.tv/gi,
  42. "www.crunchyroll.com": /www.crunchyroll.com\/.*watch\/([0-9]|[A-Z])+/gi,
  43. "beta.crunchyroll.com": /beta.crunchyroll.com\/.*watch\/([0-9]|[A-Z])+/gi,
  44. "live.bilibili.com": /live.bilibili.com\/[0-9]+/gi,
  45. "www.bilibili.com": /www.bilibili.com\/video\/(av|bv)/gi,
  46. };
  47.  
  48. const ICON_MPV =
  49. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0\
  50. PSI2NCIgdmVyc2lvbj0iMSI+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4yIiBjeD0iMzIiIGN5\
  51. PSIzMyIgcj0iMjgiLz4KIDxjaXJjbGUgc3R5bGU9ImZpbGw6IzhkMzQ4ZSIgY3g9IjMyIiBjeT0i\
  52. MzIiIHI9IjI4Ii8+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4zIiBjeD0iMzQuNSIgY3k9IjI5\
  53. LjUiIHI9IjIwLjUiLz4KIDxjaXJjbGUgc3R5bGU9Im9wYWNpdHk6LjIiIGN4PSIzMiIgY3k9IjMz\
  54. IiByPSIxNCIvPgogPGNpcmNsZSBzdHlsZT0iZmlsbDojZmZmZmZmIiBjeD0iMzIiIGN5PSIzMiIg\
  55. cj0iMTQiLz4KIDxwYXRoIHN0eWxlPSJmaWxsOiM2OTFmNjkiIHRyYW5zZm9ybT0ibWF0cml4KDEu\
  56. NTE1NTQ0NSwwLDAsMS41LC0zLjY1Mzg3OSwtNC45ODczODQ4KSIgZD0ibTI3LjE1NDUxNyAyNC42\
  57. NTgyNTctMy40NjQxMDEgMi0zLjQ2NDEwMiAxLjk5OTk5OXYtNC0zLjk5OTk5OWwzLjQ2NDEwMiAy\
  58. eiIvPgogPHBhdGggc3R5bGU9ImZpbGw6I2ZmZmZmZjtvcGFjaXR5Oi4xIiBkPSJNIDMyIDQgQSAy\
  59. OCAyOCAwIDAgMCA0IDMyIEEgMjggMjggMCAwIDAgNC4wMjE0ODQ0IDMyLjU4NTkzOCBBIDI4IDI4\
  60. IDAgMCAxIDMyIDUgQSAyOCAyOCAwIDAgMSA1OS45Nzg1MTYgMzIuNDE0MDYyIEEgMjggMjggMCAw\
  61. IDAgNjAgMzIgQSAyOCAyOCAwIDAgMCAzMiA0IHoiLz4KPC9zdmc+Cg==";
  62.  
  63. const ICON_SETTINGS =
  64. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0\
  65. PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KIDxkZWZzPgogIDxzdHlsZSBpZD0iY3VycmVudC1j\
  66. b2xvci1zY2hlbWUiIHR5cGU9InRleHQvY3NzIj4KICAgLkNvbG9yU2NoZW1lLVRleHQgeyBjb2xv\
  67. cjojNDQ0NDQ0OyB9IC5Db2xvclNjaGVtZS1IaWdobGlnaHQgeyBjb2xvcjojNDI4NWY0OyB9CiAg\
  68. PC9zdHlsZT4KIDwvZGVmcz4KIDxwYXRoIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvciIgY2xhc3M9\
  69. IkNvbG9yU2NoZW1lLVRleHQiIGQ9Ik0gNi4yNSAxIEwgNi4wOTU3MDMxIDIuODQzNzUgQSA1LjUg\
  70. NS41IDAgMCAwIDQuNDg4MjgxMiAzLjc3MzQzNzUgTCAyLjgxMjUgMi45ODQzNzUgTCAxLjA2MjUg\
  71. Ni4wMTU2MjUgTCAyLjU4Mzk4NDQgNy4wNzIyNjU2IEEgNS41IDUuNSAwIDAgMCAyLjUgOCBBIDUu\
  72. NSA1LjUgMCAwIDAgMi41ODAwNzgxIDguOTMxNjQwNiBMIDEuMDYyNSA5Ljk4NDM3NSBMIDIuODEy\
  73. NSAxMy4wMTU2MjUgTCA0LjQ4NDM3NSAxMi4yMjg1MTYgQSA1LjUgNS41IDAgMCAwIDYuMDk1NzAz\
  74. MSAxMy4xNTIzNDQgTCA2LjI0NjA5MzggMTUuMDAxOTUzIEwgOS43NDYwOTM4IDE1LjAwMTk1MyBM\
  75. IDkuOTAwMzkwNiAxMy4xNTgyMDMgQSA1LjUgNS41IDAgMCAwIDExLjUwNzgxMiAxMi4yMjg1MTYg\
  76. TCAxMy4xODM1OTQgMTMuMDE3NTc4IEwgMTQuOTMzNTk0IDkuOTg2MzI4MSBMIDEzLjQxMjEwOSA4\
  77. LjkyOTY4NzUgQSA1LjUgNS41IDAgMCAwIDEzLjQ5NjA5NCA4LjAwMTk1MzEgQSA1LjUgNS41IDAg\
  78. MCAwIDEzLjQxNjAxNiA3LjA3MDMxMjUgTCAxNC45MzM1OTQgNi4wMTc1NzgxIEwgMTMuMTgzNTk0\
  79. IDIuOTg2MzI4MSBMIDExLjUxMTcxOSAzLjc3MzQzNzUgQSA1LjUgNS41IDAgMCAwIDkuOTAwMzkw\
  80. NiAyLjg0OTYwOTQgTCA5Ljc1IDEgTCA2LjI1IDEgeiBNIDggNiBBIDIgMiAwIDAgMSAxMCA4IEEg\
  81. MiAyIDAgMCAxIDggMTAgQSAyIDIgMCAwIDEgNiA4IEEgMiAyIDAgMCAxIDggNiB6IiB0cmFuc2Zv\
  82. cm09InRyYW5zbGF0ZSg0IDQpIi8+Cjwvc3ZnPgo=";
  83.  
  84. const MPV_CSS = `
  85. .pwm-play {
  86. width: 48px;
  87. height: 48px;
  88. border: 0;
  89. border-radius: 50%;
  90. background-size: 48px;
  91. background-image: url(data:image/svg+xml;base64,${ICON_MPV});
  92. background-repeat: no-repeat;
  93. }
  94. .pwm-settings {
  95. opacity: 0;
  96. visibility: hidden;
  97. transition: all 0.2s ease-in-out;
  98. display: block;
  99. position: absolute;
  100. top: -32px;
  101. width: 32px;
  102. height: 32px;
  103. margin-left: 8px;
  104. border: 0;
  105. border-radius: 50%;
  106. background-size: 32px;
  107. background-color: #eeeeee;
  108. background-image: url(data:image/svg+xml;base64,${ICON_SETTINGS});
  109. background-repeat: no-repeat;
  110. }
  111. .play-with-mpv {
  112. z-index: 99999;
  113. position: fixed;
  114. left: 8px;
  115. bottom: 8px;
  116. }
  117. .pwm-play:hover + .pwm-settings,
  118. .pwm-settings:hover {
  119. opacity: 1;
  120. visibility: visible;
  121. transition: all 0.2s ease-in-out;
  122. }
  123. `;
  124.  
  125. const CONFIG_ID = "play-with-mpv";
  126.  
  127. const CONFIG_CSS = `
  128. body {
  129. display: flex;
  130. justify-content: center;
  131. }
  132. #${CONFIG_ID}_wrapper {
  133. display: flex;
  134. flex-direction: column;
  135. justify-content: center;
  136. }
  137. #${CONFIG_ID} .config_header {
  138. padding: 12px;
  139. }
  140. #${CONFIG_ID} .config_var {
  141. margin: 0 0 8px 0;
  142. }
  143. #${CONFIG_ID}_field_cookies,
  144. #${CONFIG_ID}_field_quality,
  145. #${CONFIG_ID}_field_v_codec {
  146. display: flex;
  147. }
  148. #${CONFIG_ID} .field_label,
  149. #${CONFIG_ID} .radio_label {
  150. font-size: 14px;
  151. }
  152. #${CONFIG_ID} input[type='radio'] {
  153. margin: 1px 4px 0 8px;
  154. }
  155. #${CONFIG_ID} .saveclose_buttons {
  156. margin: 1px;
  157. padding: 4px 16px;
  158. }
  159. #${CONFIG_ID} .reset_holder {
  160. padding-top: 4px;
  161. }
  162. `;
  163.  
  164. const CONFIG_IFRAME_CSS = `
  165. position: fixed;
  166. z-index: 99999;
  167. width: 450px;
  168. height: 280px;
  169. border: 1px solid;
  170. border-radius: 10px;
  171. `;
  172.  
  173. GM_config.init({
  174. id: `${CONFIG_ID}`,
  175. title: `${GM_info.script.name}`,
  176. fields: {
  177. cookies: {
  178. label: "Try Pass Cookies",
  179. type: "radio",
  180. options: ["yes", "no"],
  181. default: "no",
  182. },
  183. // profile: {
  184. // label: "Profile",
  185. // type: "text",
  186. // default: "",
  187. // },
  188. quality: {
  189. label: "Prefer Quality",
  190. type: "radio",
  191. options: ["best", "2160p", "1440p", "1080p", "720p", "480p", "360p"],
  192. default: "best",
  193. },
  194. v_codec: {
  195. label: "Prefer Video Codec",
  196. type: "radio",
  197. options: ["any", "av01", "vp9", "h265", "h264"],
  198. default: "any",
  199. },
  200. },
  201. events: {
  202. save: () => {
  203. updateButton(location.href);
  204. GM_config.close();
  205. },
  206. reset: () => {
  207. updateButton(location.href);
  208. GM_config.save();
  209. GM_config.close();
  210. },
  211. },
  212. css: CONFIG_CSS.trim(),
  213. });
  214.  
  215. function notifyUpdate() {
  216. let version = GM_getValue("mpvHandlerVersion", null);
  217.  
  218. if (version !== MPV_HANDLER_VERSION) {
  219. const UPDATE_NOTIFY = {
  220. title: `${GM_info.script.name}`,
  221. text: `mpv-handler is upgraded to ${MPV_HANDLER_VERSION}\n\nClick to check updates`,
  222. onclick: () => {
  223. GM_openInTab("https://github.com/akiirui/mpv-handler/releases/latest");
  224. },
  225. };
  226.  
  227. GM_notification(UPDATE_NOTIFY);
  228. GM_setValue("mpvHandlerVersion", MPV_HANDLER_VERSION);
  229. }
  230. }
  231.  
  232. function matchUrl(currentUrl) {
  233. if (MATCHERS[location.hostname]) {
  234. return currentUrl.search(MATCHERS[location.hostname]) !== -1;
  235. } else {
  236. return false;
  237. }
  238. }
  239.  
  240. function appendButton() {
  241. let head = document.getElementsByTagName("head")[0];
  242. let style = document.createElement("style");
  243.  
  244. if (head) {
  245. style.innerHTML = MPV_CSS.trim();
  246. head.appendChild(style);
  247. }
  248.  
  249. let body = document.body;
  250. let buttonDiv = document.createElement("div");
  251. let buttonPlay = document.createElement("a");
  252. let buttonSettings = document.createElement("button");
  253.  
  254. if (body) {
  255. buttonPlay.className = "pwm-play";
  256. buttonPlay.style = "display: none";
  257. buttonPlay.target = "_blank";
  258. buttonPlay.addEventListener("click", (e) => {
  259. let videoElement = document.getElementsByTagName("video")[0];
  260. if (videoElement) videoElement.pause();
  261. if (e.stopPropagation) e.stopPropagation();
  262. });
  263.  
  264. buttonSettings.className = "pwm-settings";
  265. buttonSettings.addEventListener("click", () => {
  266. if (!GM_config.isOpen) {
  267. GM_config.open();
  268. GM_config.frame.style = CONFIG_IFRAME_CSS.trim();
  269. }
  270. });
  271.  
  272. buttonDiv.className = "play-with-mpv";
  273. buttonDiv.appendChild(buttonPlay);
  274. buttonDiv.appendChild(buttonSettings);
  275.  
  276. body.appendChild(buttonDiv);
  277.  
  278. document.addEventListener("fullscreenchange", () => {
  279. let button = document.getElementsByClassName("pwm-play")[0];
  280.  
  281. button.style = document.fullscreenElement
  282. ? "display: none"
  283. : "display: block";
  284. });
  285. }
  286. }
  287.  
  288. function updateButton(currentUrl) {
  289. let isMatch = matchUrl(currentUrl);
  290. let button = document.getElementsByClassName("pwm-play")[0];
  291.  
  292. if (button) {
  293. let cookies = GM_config.get("cookies").toLowerCase();
  294. // let profile = GM_config.get("profile");
  295. let quality = GM_config.get("quality").toLowerCase();
  296. let v_codec = GM_config.get("v_codec").toLowerCase();
  297. let options = [];
  298.  
  299. let proto =
  300. "mpv://play/" + btoa(currentUrl).replace(/\//g, "_").replace(/\+/g, "-");
  301.  
  302. if (cookies === "yes") {
  303. options.push("cookies=" + document.location.hostname + ".txt");
  304. }
  305. // if (profile.trim() !== "") {
  306. // options.push("profile=" + profile);
  307. // }
  308. if (quality !== "best") {
  309. options.push("quality=" + quality);
  310. }
  311. if (v_codec !== "any") {
  312. options.push("v_codec=" + v_codec);
  313. }
  314.  
  315. if (options.length !== 0) {
  316. proto += "/?";
  317.  
  318. options.forEach((option, index) => {
  319. proto += option;
  320.  
  321. if (index + 1 !== options.length) {
  322. proto += "&";
  323. }
  324. });
  325. }
  326.  
  327. button.style = isMatch ? "display: block" : "display: none";
  328. button.href = isMatch ? proto : "";
  329. }
  330. }
  331.  
  332. function detectPJAX() {
  333. let previousUrl = null;
  334. let currentUrl = null;
  335.  
  336. setInterval(() => {
  337. currentUrl = location.href;
  338.  
  339. if (previousUrl !== currentUrl) {
  340. updateButton(currentUrl);
  341. previousUrl = currentUrl;
  342. }
  343. }, 500);
  344. }
  345.  
  346. notifyUpdate();
  347. appendButton();
  348. detectPJAX();

QingJ © 2025

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