使用 mpv 播放

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

目前为 2020-12-29 提交的版本,查看 最新版本

  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 website videos and songs with mpv & youtube-dl
  7. // @description:en-US Play website videos and songs with mpv & youtube-dl
  8. // @description:zh-CN 通过 mpv 和 youtube-dl 播放网页上的视频和歌曲
  9. // @description:zh-TW 通過 mpv 和 youtube-dl 播放網頁上的視頻和歌曲
  10. // @namespace play-with-mpv-handler
  11. // @version 2020.12.29.2
  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 *://www.bilibili.com/video/*
  24. // ==/UserScript==
  25.  
  26. "use strict";
  27.  
  28. const MPV_HANDLER_VERSION = "v0.1.2";
  29. const MATCH_URLS = ["www.youtube.com/watch", "www.bilibili.com/video/"];
  30.  
  31. const ICON_MPV =
  32. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0\
  33. PSI2NCIgdmVyc2lvbj0iMSI+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4yIiBjeD0iMzIiIGN5\
  34. PSIzMyIgcj0iMjgiLz4KIDxjaXJjbGUgc3R5bGU9ImZpbGw6IzhkMzQ4ZSIgY3g9IjMyIiBjeT0i\
  35. MzIiIHI9IjI4Ii8+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4zIiBjeD0iMzQuNSIgY3k9IjI5\
  36. LjUiIHI9IjIwLjUiLz4KIDxjaXJjbGUgc3R5bGU9Im9wYWNpdHk6LjIiIGN4PSIzMiIgY3k9IjMz\
  37. IiByPSIxNCIvPgogPGNpcmNsZSBzdHlsZT0iZmlsbDojZmZmZmZmIiBjeD0iMzIiIGN5PSIzMiIg\
  38. cj0iMTQiLz4KIDxwYXRoIHN0eWxlPSJmaWxsOiM2OTFmNjkiIHRyYW5zZm9ybT0ibWF0cml4KDEu\
  39. NTE1NTQ0NSwwLDAsMS41LC0zLjY1Mzg3OSwtNC45ODczODQ4KSIgZD0ibTI3LjE1NDUxNyAyNC42\
  40. NTgyNTctMy40NjQxMDEgMi0zLjQ2NDEwMiAxLjk5OTk5OXYtNC0zLjk5OTk5OWwzLjQ2NDEwMiAy\
  41. eiIvPgogPHBhdGggc3R5bGU9ImZpbGw6I2ZmZmZmZjtvcGFjaXR5Oi4xIiBkPSJNIDMyIDQgQSAy\
  42. OCAyOCAwIDAgMCA0IDMyIEEgMjggMjggMCAwIDAgNC4wMjE0ODQ0IDMyLjU4NTkzOCBBIDI4IDI4\
  43. IDAgMCAxIDMyIDUgQSAyOCAyOCAwIDAgMSA1OS45Nzg1MTYgMzIuNDE0MDYyIEEgMjggMjggMCAw\
  44. IDAgNjAgMzIgQSAyOCAyOCAwIDAgMCAzMiA0IHoiLz4KPC9zdmc+Cg==";
  45.  
  46. const ICON_SETTINGS =
  47. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0\
  48. PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KIDxkZWZzPgogIDxzdHlsZSBpZD0iY3VycmVudC1j\
  49. b2xvci1zY2hlbWUiIHR5cGU9InRleHQvY3NzIj4KICAgLkNvbG9yU2NoZW1lLVRleHQgeyBjb2xv\
  50. cjojNDQ0NDQ0OyB9IC5Db2xvclNjaGVtZS1IaWdobGlnaHQgeyBjb2xvcjojNDI4NWY0OyB9CiAg\
  51. PC9zdHlsZT4KIDwvZGVmcz4KIDxwYXRoIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvciIgY2xhc3M9\
  52. IkNvbG9yU2NoZW1lLVRleHQiIGQ9Ik0gNi4yNSAxIEwgNi4wOTU3MDMxIDIuODQzNzUgQSA1LjUg\
  53. NS41IDAgMCAwIDQuNDg4MjgxMiAzLjc3MzQzNzUgTCAyLjgxMjUgMi45ODQzNzUgTCAxLjA2MjUg\
  54. Ni4wMTU2MjUgTCAyLjU4Mzk4NDQgNy4wNzIyNjU2IEEgNS41IDUuNSAwIDAgMCAyLjUgOCBBIDUu\
  55. NSA1LjUgMCAwIDAgMi41ODAwNzgxIDguOTMxNjQwNiBMIDEuMDYyNSA5Ljk4NDM3NSBMIDIuODEy\
  56. NSAxMy4wMTU2MjUgTCA0LjQ4NDM3NSAxMi4yMjg1MTYgQSA1LjUgNS41IDAgMCAwIDYuMDk1NzAz\
  57. MSAxMy4xNTIzNDQgTCA2LjI0NjA5MzggMTUuMDAxOTUzIEwgOS43NDYwOTM4IDE1LjAwMTk1MyBM\
  58. IDkuOTAwMzkwNiAxMy4xNTgyMDMgQSA1LjUgNS41IDAgMCAwIDExLjUwNzgxMiAxMi4yMjg1MTYg\
  59. TCAxMy4xODM1OTQgMTMuMDE3NTc4IEwgMTQuOTMzNTk0IDkuOTg2MzI4MSBMIDEzLjQxMjEwOSA4\
  60. LjkyOTY4NzUgQSA1LjUgNS41IDAgMCAwIDEzLjQ5NjA5NCA4LjAwMTk1MzEgQSA1LjUgNS41IDAg\
  61. MCAwIDEzLjQxNjAxNiA3LjA3MDMxMjUgTCAxNC45MzM1OTQgNi4wMTc1NzgxIEwgMTMuMTgzNTk0\
  62. IDIuOTg2MzI4MSBMIDExLjUxMTcxOSAzLjc3MzQzNzUgQSA1LjUgNS41IDAgMCAwIDkuOTAwMzkw\
  63. NiAyLjg0OTYwOTQgTCA5Ljc1IDEgTCA2LjI1IDEgeiBNIDggNiBBIDIgMiAwIDAgMSAxMCA4IEEg\
  64. MiAyIDAgMCAxIDggMTAgQSAyIDIgMCAwIDEgNiA4IEEgMiAyIDAgMCAxIDggNiB6IiB0cmFuc2Zv\
  65. cm09InRyYW5zbGF0ZSg0IDQpIi8+Cjwvc3ZnPgo=";
  66.  
  67. const MPV_CSS = `
  68. .pwm-play {
  69. width: 48px;
  70. height: 48px;
  71. border: 0;
  72. border-radius: 50%;
  73. background-size: 48px;
  74. background-image: url(data:image/svg+xml;base64,${ICON_MPV});
  75. background-repeat: no-repeat;
  76. }
  77. .pwm-settings {
  78. display: block;
  79. transform: translate(25%, 0);
  80. width: 32px;
  81. height: 32px;
  82. background-color: #eeeeee;
  83. border: 0;
  84. border-radius: 50%;
  85. background-size: 32px;
  86. background-image: url(data:image/svg+xml;base64,${ICON_SETTINGS});
  87. background-repeat: no-repeat;
  88. visibility: hidden;
  89. opacity: 0;
  90. transition: opacity 0.3s ease-in-out;
  91. }
  92. .pwm-iframe {
  93. display: none;
  94. }
  95. .play-with-mpv {
  96. position: fixed;
  97. left: 12px;
  98. bottom: 12px;
  99. }
  100. .play-with-mpv:hover .pwm-settings {
  101. visibility: visible;
  102. opacity: 1;
  103. transition: opacity 0.3s ease-in-out;
  104. }
  105. `;
  106.  
  107. const CONFIG_ID = "perferQuality";
  108.  
  109. const CONFIG_CSS = `
  110. #${CONFIG_ID} .config_header {
  111. font-size: 1.5em;
  112. margin: 0;
  113. padding-bottom: 0.3em;
  114. }
  115. #${CONFIG_ID} .field_label {
  116. font-size: 1em;
  117. }
  118. #${CONFIG_ID} .radio_label {
  119. font-size: 0.9em;
  120. }
  121. #${CONFIG_ID} .saveclose_buttons {
  122. margin: 0.2em;
  123. padding: 0.3em 1.5em;
  124. }
  125. #${CONFIG_ID}_buttons_holder {
  126. position: fixed;
  127. width: 97%;
  128. bottom: 0;
  129. }
  130. #${CONFIG_ID} .reset_holder {
  131. float: left;
  132. position: relative;
  133. bottom: -1em;
  134. }
  135. #${CONFIG_ID}_field_perferQuality {
  136. padding-top: 0.3em;
  137. }
  138. `;
  139.  
  140. const IFRAME_CSS = `
  141. height: 140px;
  142. width: 290px;
  143. border: 1px solid;
  144. border-radius: 3px;
  145. position: fixed;
  146. z-index: 999;
  147. `;
  148.  
  149. GM_config.init({
  150. id: `${CONFIG_ID}`,
  151. title: `${GM_info.script.name}`,
  152. fields: {
  153. perferQuality: {
  154. label: "Prefer quality",
  155. type: "radio",
  156. options: ["Best", "4K", "2K", "1080P", "720P"],
  157. default: "Best",
  158. },
  159. },
  160. events: {
  161. save: () => {
  162. updateButton(location.href);
  163. },
  164. reset: () => {
  165. updateButton(location.href);
  166. },
  167. },
  168. css: CONFIG_CSS,
  169. });
  170.  
  171. function notifyHandlerUpdate() {
  172. const updateNotify = {
  173. title: `${GM_info.script.name}`,
  174. text: `mpv-handler is upgraded to ${MPV_HANDLER_VERSION}\n\nClick to check updates`,
  175. onclick: () => {
  176. GM_openInTab("https://github.com/akiirui/mpv-handler/releases/latest");
  177. GM_setValue("mpvHandlerVersion", MPV_HANDLER_VERSION);
  178. },
  179. };
  180.  
  181. let version = GM_getValue("mpvHandlerVersion", null);
  182. if (version !== MPV_HANDLER_VERSION) {
  183. GM_notification(updateNotify);
  184. }
  185. }
  186.  
  187. function appendButton() {
  188. let head = document.getElementsByTagName("head")[0];
  189. let style = document.createElement("style");
  190.  
  191. if (head) {
  192. style.innerHTML = MPV_CSS;
  193. head.appendChild(style);
  194. }
  195.  
  196. let body = document.getElementsByTagName("body")[0];
  197. let buttonDiv = document.createElement("div");
  198. let buttonSettings = document.createElement("button");
  199. let buttonPlay = document.createElement("a");
  200. let buttonIframe = document.createElement("iframe");
  201.  
  202. if (body) {
  203. buttonSettings.className = "pwm-settings";
  204. buttonSettings.addEventListener("click", () => {
  205. if (!GM_config.isOpen) {
  206. GM_config.open();
  207. GM_config.frame.style = IFRAME_CSS;
  208. }
  209. });
  210.  
  211. buttonPlay.className = "pwm-play";
  212. buttonPlay.target = "pwm-iframe";
  213. buttonPlay.style = "display: none";
  214. buttonPlay.addEventListener("click", () => {
  215. let videoElement = document.getElementsByTagName("video")[0];
  216. if (videoElement) videoElement.pause();
  217. });
  218.  
  219. buttonIframe.className = "pwm-iframe";
  220. buttonIframe.name = "pwm-iframe";
  221.  
  222. buttonDiv.className = "play-with-mpv";
  223. buttonDiv.appendChild(buttonSettings);
  224. buttonDiv.appendChild(buttonPlay);
  225. buttonDiv.appendChild(buttonIframe);
  226.  
  227. body.appendChild(buttonDiv);
  228. }
  229. }
  230.  
  231. function updateButton(currentUrl) {
  232. let isMatch = false;
  233. let button = document.getElementsByClassName("pwm-play")[0];
  234.  
  235. for (const element of MATCH_URLS) {
  236. if ((isMatch = currentUrl.includes(element))) break;
  237. }
  238.  
  239. if (button) {
  240. let quality = GM_config.get("perferQuality");
  241. let protocol = currentUrl + "|" + quality;
  242.  
  243. button.style = isMatch ? "display: block" : "display: none";
  244. button.href = isMatch ? "mpv://" + btoa(protocol) : "";
  245. console.log(protocol);
  246. }
  247. }
  248.  
  249. function detectPJAX() {
  250. let previousUrl = null;
  251.  
  252. setInterval(() => {
  253. let currentUrl = location.href;
  254.  
  255. if (currentUrl && previousUrl !== currentUrl) {
  256. updateButton(currentUrl);
  257. previousUrl = currentUrl;
  258. }
  259. }, 500);
  260. }
  261.  
  262. notifyHandlerUpdate();
  263. appendButton();
  264. detectPJAX();