Play with MPV (NSFW)

Play videos and songs on the website via mpv-handler (NSFW)

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