使用 mpv 播放

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

当前为 2021-02-22 提交的版本,查看 最新版本

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