使用 mpv 播放

通過 mpv 和 youtube-dl 播放網頁上的視頻和歌曲

目前為 2021-01-14 提交的版本,檢視 最新版本

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