Greasy Fork is available in English.

Play with MPV

Play videos and songs on the website via mpv-handler

  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 2025.03.20
  12. // @author Akatsuki Rui
  13. // @license MIT License
  14. // @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@06f2015c04db3aaab9717298394ca4f025802873/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.13";
  31.  
  32. const allow = true;
  33. const block = false;
  34. const SITE_YOUTUBE = {
  35. mode: allow,
  36. list: ["/watch", "/playlist", "/shorts"],
  37. };
  38.  
  39. const SITE_TWITCH = {
  40. mode: block,
  41. list: ["/directory", "/downloads", "/jobs", "/p", "/turbo"],
  42. };
  43. const SITE_CRUNCHYROLL = {
  44. mode: allow,
  45. list: ["/watch"],
  46. };
  47. const SITE_BILIBILI = {
  48. mode: allow,
  49. list: ["/bangumi/play", "/video"],
  50. };
  51. const SITE_BILIBILI_LIVE = {
  52. mode: block,
  53. list: ["/p"],
  54. };
  55. const SITE_KICK = {
  56. mode: block,
  57. list: ["/browse", "/category"],
  58. };
  59.  
  60. const MATCHERS = {
  61. "www.youtube.com": SITE_YOUTUBE,
  62. "m.youtube.com": SITE_YOUTUBE,
  63. "www.twitch.tv": SITE_TWITCH,
  64. "www.crunchyroll.com": SITE_CRUNCHYROLL,
  65. "www.bilibili.com": SITE_BILIBILI,
  66. "live.bilibili.com": SITE_BILIBILI_LIVE,
  67. "kick.com": SITE_KICK,
  68. };
  69.  
  70. const ICON_MPV =
  71. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0\
  72. PSI2NCIgdmVyc2lvbj0iMSI+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4yIiBjeD0iMzIiIGN5\
  73. PSIzMyIgcj0iMjgiLz4KIDxjaXJjbGUgc3R5bGU9ImZpbGw6IzhkMzQ4ZSIgY3g9IjMyIiBjeT0i\
  74. MzIiIHI9IjI4Ii8+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4zIiBjeD0iMzQuNSIgY3k9IjI5\
  75. LjUiIHI9IjIwLjUiLz4KIDxjaXJjbGUgc3R5bGU9Im9wYWNpdHk6LjIiIGN4PSIzMiIgY3k9IjMz\
  76. IiByPSIxNCIvPgogPGNpcmNsZSBzdHlsZT0iZmlsbDojZmZmZmZmIiBjeD0iMzIiIGN5PSIzMiIg\
  77. cj0iMTQiLz4KIDxwYXRoIHN0eWxlPSJmaWxsOiM2OTFmNjkiIHRyYW5zZm9ybT0ibWF0cml4KDEu\
  78. NTE1NTQ0NSwwLDAsMS41LC0zLjY1Mzg3OSwtNC45ODczODQ4KSIgZD0ibTI3LjE1NDUxNyAyNC42\
  79. NTgyNTctMy40NjQxMDEgMi0zLjQ2NDEwMiAxLjk5OTk5OXYtNC0zLjk5OTk5OWwzLjQ2NDEwMiAy\
  80. eiIvPgogPHBhdGggc3R5bGU9ImZpbGw6I2ZmZmZmZjtvcGFjaXR5Oi4xIiBkPSJNIDMyIDQgQSAy\
  81. OCAyOCAwIDAgMCA0IDMyIEEgMjggMjggMCAwIDAgNC4wMjE0ODQ0IDMyLjU4NTkzOCBBIDI4IDI4\
  82. IDAgMCAxIDMyIDUgQSAyOCAyOCAwIDAgMSA1OS45Nzg1MTYgMzIuNDE0MDYyIEEgMjggMjggMCAw\
  83. IDAgNjAgMzIgQSAyOCAyOCAwIDAgMCAzMiA0IHoiLz4KPC9zdmc+Cg==";
  84.  
  85. const ICON_SETTINGS =
  86. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0\
  87. PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KIDxkZWZzPgogIDxzdHlsZSBpZD0iY3VycmVudC1j\
  88. b2xvci1zY2hlbWUiIHR5cGU9InRleHQvY3NzIj4KICAgLkNvbG9yU2NoZW1lLVRleHQgeyBjb2xv\
  89. cjojNDQ0NDQ0OyB9IC5Db2xvclNjaGVtZS1IaWdobGlnaHQgeyBjb2xvcjojNDI4NWY0OyB9CiAg\
  90. PC9zdHlsZT4KIDwvZGVmcz4KIDxwYXRoIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvciIgY2xhc3M9\
  91. IkNvbG9yU2NoZW1lLVRleHQiIGQ9Ik0gNi4yNSAxIEwgNi4wOTU3MDMxIDIuODQzNzUgQSA1LjUg\
  92. NS41IDAgMCAwIDQuNDg4MjgxMiAzLjc3MzQzNzUgTCAyLjgxMjUgMi45ODQzNzUgTCAxLjA2MjUg\
  93. Ni4wMTU2MjUgTCAyLjU4Mzk4NDQgNy4wNzIyNjU2IEEgNS41IDUuNSAwIDAgMCAyLjUgOCBBIDUu\
  94. NSA1LjUgMCAwIDAgMi41ODAwNzgxIDguOTMxNjQwNiBMIDEuMDYyNSA5Ljk4NDM3NSBMIDIuODEy\
  95. NSAxMy4wMTU2MjUgTCA0LjQ4NDM3NSAxMi4yMjg1MTYgQSA1LjUgNS41IDAgMCAwIDYuMDk1NzAz\
  96. MSAxMy4xNTIzNDQgTCA2LjI0NjA5MzggMTUuMDAxOTUzIEwgOS43NDYwOTM4IDE1LjAwMTk1MyBM\
  97. IDkuOTAwMzkwNiAxMy4xNTgyMDMgQSA1LjUgNS41IDAgMCAwIDExLjUwNzgxMiAxMi4yMjg1MTYg\
  98. TCAxMy4xODM1OTQgMTMuMDE3NTc4IEwgMTQuOTMzNTk0IDkuOTg2MzI4MSBMIDEzLjQxMjEwOSA4\
  99. LjkyOTY4NzUgQSA1LjUgNS41IDAgMCAwIDEzLjQ5NjA5NCA4LjAwMTk1MzEgQSA1LjUgNS41IDAg\
  100. MCAwIDEzLjQxNjAxNiA3LjA3MDMxMjUgTCAxNC45MzM1OTQgNi4wMTc1NzgxIEwgMTMuMTgzNTk0\
  101. IDIuOTg2MzI4MSBMIDExLjUxMTcxOSAzLjc3MzQzNzUgQSA1LjUgNS41IDAgMCAwIDkuOTAwMzkw\
  102. NiAyLjg0OTYwOTQgTCA5Ljc1IDEgTCA2LjI1IDEgeiBNIDggNiBBIDIgMiAwIDAgMSAxMCA4IEEg\
  103. MiAyIDAgMCAxIDggMTAgQSAyIDIgMCAwIDEgNiA4IEEgMiAyIDAgMCAxIDggNiB6IiB0cmFuc2Zv\
  104. cm09InRyYW5zbGF0ZSg0IDQpIi8+Cjwvc3ZnPgo=";
  105.  
  106. const css = String.raw;
  107.  
  108. const MPV_CSS = css`
  109. .pwm-play {
  110. width: 48px;
  111. height: 48px;
  112. border: 0;
  113. border-radius: 50%;
  114. background-size: 48px;
  115. background-image: url(data:image/svg+xml;base64,${ICON_MPV});
  116. background-repeat: no-repeat;
  117. }
  118. .pwm-settings {
  119. opacity: 0;
  120. visibility: hidden;
  121. transition: all 0.2s ease-in-out;
  122. display: block;
  123. position: absolute;
  124. top: -32px;
  125. width: 32px;
  126. height: 32px;
  127. margin-left: 8px;
  128. border: 0;
  129. border-radius: 50%;
  130. background-size: 32px;
  131. background-color: #eeeeee;
  132. background-image: url(data:image/svg+xml;base64,${ICON_SETTINGS});
  133. background-repeat: no-repeat;
  134. }
  135. .play-with-mpv {
  136. z-index: 99999;
  137. position: fixed;
  138. left: 8px;
  139. bottom: 8px;
  140. }
  141. .pwm-play:hover + .pwm-settings,
  142. .pwm-settings:hover {
  143. opacity: 1;
  144. visibility: visible;
  145. transition: all 0.2s ease-in-out;
  146. }
  147. `;
  148.  
  149. const CONFIG_ID = "play-with-mpv";
  150.  
  151. const CONFIG_CSS = css`
  152. body {
  153. display: flex;
  154. justify-content: center;
  155. }
  156. #${CONFIG_ID}_wrapper {
  157. display: flex;
  158. flex-direction: column;
  159. justify-content: center;
  160. }
  161. #${CONFIG_ID} .config_header {
  162. display: flex;
  163. align-items: center;
  164. padding: 12px;
  165. }
  166. #${CONFIG_ID} .config_var {
  167. margin: 0 0 12px 0;
  168. }
  169. #${CONFIG_ID} .field_label {
  170. display: inline-block;
  171. width: 140px;
  172. font-size: 14px;
  173. }
  174. #${CONFIG_ID}_field_cookies,
  175. #${CONFIG_ID}_field_profile,
  176. #${CONFIG_ID}_field_quality,
  177. #${CONFIG_ID}_field_v_codec,
  178. #${CONFIG_ID}_field_console {
  179. width: 80px;
  180. height: 24px;
  181. font-size: 14px;
  182. text-align: center;
  183. }
  184. #${CONFIG_ID}_buttons_holder {
  185. display: flex;
  186. flex-direction: column;
  187. }
  188. #${CONFIG_ID} .saveclose_buttons {
  189. margin: 1px;
  190. padding: 4px 0;
  191. }
  192. #${CONFIG_ID} .reset_holder {
  193. padding-top: 4px;
  194. }
  195. `;
  196.  
  197. const CONFIG_IFRAME_CSS = css`
  198. position: fixed;
  199. z-index: 99999;
  200. width: 300px;
  201. height: 400px;
  202. border: 1px solid;
  203. border-radius: 10px;
  204. `;
  205.  
  206. const CONFIG_FIELDS = {
  207. cookies: {
  208. label: "Try Pass Cookies",
  209. type: "select",
  210. options: ["yes", "no"],
  211. default: "no",
  212. },
  213. profile: {
  214. label: "MPV Profile",
  215. type: "text",
  216. default: "default",
  217. },
  218. quality: {
  219. label: "Prefer Video Quality",
  220. type: "select",
  221. options: ["default", "2160p", "1440p", "1080p", "720p", "480p", "360p"],
  222. default: "default",
  223. },
  224. v_codec: {
  225. label: "Prefer Video Codec",
  226. type: "select",
  227. options: ["default", "av01", "vp9", "h265", "h264"],
  228. default: "default",
  229. },
  230. console: {
  231. label: "Run With Console",
  232. type: "select",
  233. options: ["yes", "no"],
  234. default: "yes",
  235. },
  236. };
  237.  
  238. // GM_config init
  239. GM_config.init({
  240. id: CONFIG_ID,
  241. title: GM_info.script.name,
  242. fields: CONFIG_FIELDS,
  243. events: {
  244. init: () => {
  245. let quality = GM_config.get("quality").toLowerCase();
  246. let v_codec = GM_config.get("v_codec").toLowerCase();
  247.  
  248. if (!CONFIG_FIELDS.quality.options.includes(quality)) {
  249. GM_config.set("quality", "default");
  250. }
  251. if (!CONFIG_FIELDS.v_codec.options.includes(v_codec)) {
  252. GM_config.set("v_codec", "default");
  253. }
  254. },
  255. save: () => {
  256. let profile = GM_config.get("profile").trim();
  257.  
  258. if (profile === "") {
  259. GM_config.set("profile", "default");
  260. } else {
  261. GM_config.set("profile", profile);
  262. }
  263.  
  264. updateButton();
  265. GM_config.close();
  266. },
  267. reset: () => {
  268. GM_config.save();
  269. },
  270. },
  271. css: CONFIG_CSS.trim(),
  272. });
  273.  
  274. // URL-safe base64 encode
  275. function btoaUrl(url) {
  276. return btoa(url).replace(/\//g, "_").replace(/\+/g, "-").replace(/\=/g, "");
  277. }
  278.  
  279. // Generate protocol
  280. function generateProto(url) {
  281. let cookies = GM_config.get("cookies").toLowerCase();
  282. let profile = GM_config.get("profile").trim();
  283. let quality = GM_config.get("quality").toLowerCase();
  284. let v_codec = GM_config.get("v_codec").toLowerCase();
  285. let console = GM_config.get("console").toLowerCase();
  286. let options = [];
  287.  
  288. let proto;
  289.  
  290. if (console === "yes") {
  291. proto = "mpv-debug://play/" + btoaUrl(url);
  292. } else {
  293. proto = "mpv://play/" + btoaUrl(url);
  294. }
  295. if (cookies === "yes") {
  296. options.push("cookies=" + document.location.hostname + ".txt");
  297. }
  298. if (profile !== "default" && profile !== "") {
  299. options.push("profile=" + profile);
  300. }
  301. if (quality !== "default") {
  302. options.push("quality=" + quality);
  303. }
  304. if (v_codec !== "default") {
  305. options.push("v_codec=" + v_codec);
  306. }
  307.  
  308. if (options.length !== 0) {
  309. proto += "/?";
  310.  
  311. options.forEach((option, index) => {
  312. proto += option;
  313.  
  314. if (index + 1 !== options.length) {
  315. proto += "&";
  316. }
  317. });
  318. }
  319.  
  320. return proto;
  321. }
  322.  
  323. // Check the URL is matched or not
  324. function matchUrl() {
  325. if (MATCHERS[location.hostname]) {
  326. let site = MATCHERS[location.hostname];
  327. let path = location.pathname;
  328.  
  329. for (const item of site.list) {
  330. if (path.startsWith(item)) {
  331. if (
  332. path.charAt(item.length) === "/" ||
  333. path.charAt(item.length) === ""
  334. ) {
  335. return site.mode;
  336. }
  337. }
  338. }
  339.  
  340. if (path !== "/") {
  341. return !site.mode;
  342. }
  343.  
  344. return false;
  345. }
  346. }
  347.  
  348. // Update button display status and URL
  349. function updateButton() {
  350. let isMatch = matchUrl();
  351. let button = document.getElementsByClassName("pwm-play")[0];
  352.  
  353. if (button) {
  354. button.style =
  355. isMatch && !document.fullscreenElement
  356. ? "display: block"
  357. : "display: none";
  358. button.href = isMatch ? generateProto(location.href) : "";
  359. }
  360. }
  361.  
  362. // Notify update about mpv-handler
  363. function notifyUpdate() {
  364. let version = GM_getValue("mpvHandlerVersion", null);
  365.  
  366. if (version !== MPV_HANDLER_VERSION) {
  367. const UPDATE_NOTIFY = {
  368. title: `${GM_info.script.name}`,
  369. text: `mpv-handler is upgraded to ${MPV_HANDLER_VERSION}\n\nClick to check updates`,
  370. onclick: () => {
  371. window.open("https://github.com/akiirui/mpv-handler/releases/latest");
  372. },
  373. };
  374.  
  375. GM_notification(UPDATE_NOTIFY);
  376. GM_setValue("mpvHandlerVersion", MPV_HANDLER_VERSION);
  377. }
  378. }
  379.  
  380. // Add play and settings buttons to page
  381. function createButton() {
  382. let head = document.getElementsByTagName("head")[0];
  383. let style = document.createElement("style");
  384.  
  385. if (head) {
  386. style.textContent = MPV_CSS.trim();
  387. head.appendChild(style);
  388. }
  389.  
  390. let body = document.body;
  391. let buttonDiv = document.createElement("div");
  392. let buttonPlay = document.createElement("a");
  393. let buttonSettings = document.createElement("button");
  394.  
  395. let pauseVideo = (e) => {
  396. let videoElement = document.getElementsByTagName("video")[0];
  397. if (videoElement) {
  398. videoElement.pause();
  399. } else {
  400. setTimeout(pauseVideo, 500, e);
  401. }
  402. if (e.stopPropagation) e.stopPropagation();
  403. };
  404.  
  405. if (body) {
  406. buttonPlay.className = "pwm-play";
  407. buttonPlay.style = "display: none";
  408. buttonPlay.addEventListener("click", pauseVideo);
  409.  
  410. buttonSettings.className = "pwm-settings";
  411. buttonSettings.addEventListener("click", () => {
  412. if (!GM_config.isOpen) {
  413. GM_config.open();
  414. GM_config.frame.style = CONFIG_IFRAME_CSS.trim();
  415. }
  416. });
  417.  
  418. buttonDiv.className = "play-with-mpv";
  419. buttonDiv.appendChild(buttonPlay);
  420. buttonDiv.appendChild(buttonSettings);
  421.  
  422. body.appendChild(buttonDiv);
  423.  
  424. document.addEventListener("fullscreenchange", () => {
  425. let button = document.getElementsByClassName("pwm-play")[0];
  426.  
  427. button.style = document.fullscreenElement
  428. ? "display: none"
  429. : "display: block";
  430. });
  431. }
  432. }
  433.  
  434. // Detect PJAX changes
  435. function detectPJAX() {
  436. let previousUrl = null;
  437. let currentUrl = null;
  438.  
  439. setInterval(() => {
  440. currentUrl = location.href;
  441.  
  442. if (previousUrl !== currentUrl) {
  443. updateButton();
  444. previousUrl = currentUrl;
  445. }
  446. }, 500);
  447. }
  448.  
  449. // Fix TrustedHTML
  450. if (window.trustedTypes && window.trustedTypes.createPolicy) {
  451. window.trustedTypes.createPolicy("default", {
  452. createHTML: (string) => string,
  453. });
  454. }
  455.  
  456. notifyUpdate();
  457. createButton();
  458. detectPJAX();