Greasy Fork 还支持 简体中文。

使用 MPV 播放

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

目前為 2023-07-04 提交的版本,檢視 最新版本

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