使用 MPV 播放

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

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