Play videos and songs on the website via mpv-handler
// ==UserScript== // @name Play with MPV // @name:en-US Play with MPV // @name:zh-CN 使用 MPV 播放 // @name:zh-TW 使用 MPV 播放 // @description Play videos and songs on the website via mpv-handler // @description:en-US Play videos and songs on the website via mpv-handler // @description:zh-CN 通过 mpv-handler 播放网页上的视频和歌曲 // @description:zh-TW 通過 mpv-handler 播放網頁上的視頻和歌曲 // @namespace play-with-mpv-handler // @version 2024.09.27 // @author Akatsuki Rui // @license MIT License // @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@06f2015c04db3aaab9717298394ca4f025802873/gm_config.js // @grant GM_info // @grant GM_getValue // @grant GM_setValue // @grant GM_notification // @run-at document-idle // @noframes // @match *://*.youtube.com/* // @match *://*.twitch.tv/* // @match *://*.crunchyroll.com/* // @match *://*.bilibili.com/* // @match *://*.kick.com/* // ==/UserScript== "use strict"; const MPV_HANDLER_VERSION = "v0.3.12"; const allow = true; const block = false; const SITE_YOUTUBE = { mode: allow, list: ["/watch", "/playlist", "/shorts"], }; const SITE_TWITCH = { mode: block, list: ["/directory", "/downloads", "/jobs", "/p", "/turbo"], }; const SITE_CRUNCHYROLL = { mode: allow, list: ["/watch"], }; const SITE_BILIBILI = { mode: allow, list: ["/bangumi/play", "/video"], }; const SITE_BILIBILI_LIVE = { mode: block, list: ["/p"], }; const SITE_KICK = { mode: block, list: ["/browse", "/category"], }; const MATCHERS = { "www.youtube.com": SITE_YOUTUBE, "m.youtube.com": SITE_YOUTUBE, "www.twitch.tv": SITE_TWITCH, "www.crunchyroll.com": SITE_CRUNCHYROLL, "www.bilibili.com": SITE_BILIBILI, "live.bilibili.com": SITE_BILIBILI_LIVE, "kick.com": SITE_KICK, }; const ICON_MPV = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0\ PSI2NCIgdmVyc2lvbj0iMSI+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4yIiBjeD0iMzIiIGN5\ PSIzMyIgcj0iMjgiLz4KIDxjaXJjbGUgc3R5bGU9ImZpbGw6IzhkMzQ4ZSIgY3g9IjMyIiBjeT0i\ MzIiIHI9IjI4Ii8+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4zIiBjeD0iMzQuNSIgY3k9IjI5\ LjUiIHI9IjIwLjUiLz4KIDxjaXJjbGUgc3R5bGU9Im9wYWNpdHk6LjIiIGN4PSIzMiIgY3k9IjMz\ IiByPSIxNCIvPgogPGNpcmNsZSBzdHlsZT0iZmlsbDojZmZmZmZmIiBjeD0iMzIiIGN5PSIzMiIg\ cj0iMTQiLz4KIDxwYXRoIHN0eWxlPSJmaWxsOiM2OTFmNjkiIHRyYW5zZm9ybT0ibWF0cml4KDEu\ NTE1NTQ0NSwwLDAsMS41LC0zLjY1Mzg3OSwtNC45ODczODQ4KSIgZD0ibTI3LjE1NDUxNyAyNC42\ NTgyNTctMy40NjQxMDEgMi0zLjQ2NDEwMiAxLjk5OTk5OXYtNC0zLjk5OTk5OWwzLjQ2NDEwMiAy\ eiIvPgogPHBhdGggc3R5bGU9ImZpbGw6I2ZmZmZmZjtvcGFjaXR5Oi4xIiBkPSJNIDMyIDQgQSAy\ OCAyOCAwIDAgMCA0IDMyIEEgMjggMjggMCAwIDAgNC4wMjE0ODQ0IDMyLjU4NTkzOCBBIDI4IDI4\ IDAgMCAxIDMyIDUgQSAyOCAyOCAwIDAgMSA1OS45Nzg1MTYgMzIuNDE0MDYyIEEgMjggMjggMCAw\ IDAgNjAgMzIgQSAyOCAyOCAwIDAgMCAzMiA0IHoiLz4KPC9zdmc+Cg=="; const ICON_SETTINGS = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0\ PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KIDxkZWZzPgogIDxzdHlsZSBpZD0iY3VycmVudC1j\ b2xvci1zY2hlbWUiIHR5cGU9InRleHQvY3NzIj4KICAgLkNvbG9yU2NoZW1lLVRleHQgeyBjb2xv\ cjojNDQ0NDQ0OyB9IC5Db2xvclNjaGVtZS1IaWdobGlnaHQgeyBjb2xvcjojNDI4NWY0OyB9CiAg\ PC9zdHlsZT4KIDwvZGVmcz4KIDxwYXRoIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvciIgY2xhc3M9\ IkNvbG9yU2NoZW1lLVRleHQiIGQ9Ik0gNi4yNSAxIEwgNi4wOTU3MDMxIDIuODQzNzUgQSA1LjUg\ NS41IDAgMCAwIDQuNDg4MjgxMiAzLjc3MzQzNzUgTCAyLjgxMjUgMi45ODQzNzUgTCAxLjA2MjUg\ Ni4wMTU2MjUgTCAyLjU4Mzk4NDQgNy4wNzIyNjU2IEEgNS41IDUuNSAwIDAgMCAyLjUgOCBBIDUu\ NSA1LjUgMCAwIDAgMi41ODAwNzgxIDguOTMxNjQwNiBMIDEuMDYyNSA5Ljk4NDM3NSBMIDIuODEy\ NSAxMy4wMTU2MjUgTCA0LjQ4NDM3NSAxMi4yMjg1MTYgQSA1LjUgNS41IDAgMCAwIDYuMDk1NzAz\ MSAxMy4xNTIzNDQgTCA2LjI0NjA5MzggMTUuMDAxOTUzIEwgOS43NDYwOTM4IDE1LjAwMTk1MyBM\ IDkuOTAwMzkwNiAxMy4xNTgyMDMgQSA1LjUgNS41IDAgMCAwIDExLjUwNzgxMiAxMi4yMjg1MTYg\ TCAxMy4xODM1OTQgMTMuMDE3NTc4IEwgMTQuOTMzNTk0IDkuOTg2MzI4MSBMIDEzLjQxMjEwOSA4\ LjkyOTY4NzUgQSA1LjUgNS41IDAgMCAwIDEzLjQ5NjA5NCA4LjAwMTk1MzEgQSA1LjUgNS41IDAg\ MCAwIDEzLjQxNjAxNiA3LjA3MDMxMjUgTCAxNC45MzM1OTQgNi4wMTc1NzgxIEwgMTMuMTgzNTk0\ IDIuOTg2MzI4MSBMIDExLjUxMTcxOSAzLjc3MzQzNzUgQSA1LjUgNS41IDAgMCAwIDkuOTAwMzkw\ NiAyLjg0OTYwOTQgTCA5Ljc1IDEgTCA2LjI1IDEgeiBNIDggNiBBIDIgMiAwIDAgMSAxMCA4IEEg\ MiAyIDAgMCAxIDggMTAgQSAyIDIgMCAwIDEgNiA4IEEgMiAyIDAgMCAxIDggNiB6IiB0cmFuc2Zv\ cm09InRyYW5zbGF0ZSg0IDQpIi8+Cjwvc3ZnPgo="; const css = String.raw; const MPV_CSS = css` .pwm-play { width: 48px; height: 48px; border: 0; border-radius: 50%; background-size: 48px; background-image: url(data:image/svg+xml;base64,${ICON_MPV}); background-repeat: no-repeat; } .pwm-settings { opacity: 0; visibility: hidden; transition: all 0.2s ease-in-out; display: block; position: absolute; top: -32px; width: 32px; height: 32px; margin-left: 8px; border: 0; border-radius: 50%; background-size: 32px; background-color: #eeeeee; background-image: url(data:image/svg+xml;base64,${ICON_SETTINGS}); background-repeat: no-repeat; } .play-with-mpv { z-index: 99999; position: fixed; left: 8px; bottom: 8px; } .pwm-play:hover + .pwm-settings, .pwm-settings:hover { opacity: 1; visibility: visible; transition: all 0.2s ease-in-out; } `; const CONFIG_ID = "play-with-mpv"; const CONFIG_CSS = css` body { display: flex; justify-content: center; } #${CONFIG_ID}_wrapper { display: flex; flex-direction: column; justify-content: center; } #${CONFIG_ID} .config_header { display: flex; align-items: center; padding: 12px; } #${CONFIG_ID} .config_var { margin: 0 0 12px 0; } #${CONFIG_ID} .field_label { display: inline-block; width: 140px; font-size: 14px; } #${CONFIG_ID}_field_cookies, #${CONFIG_ID}_field_profile, #${CONFIG_ID}_field_quality, #${CONFIG_ID}_field_v_codec, #${CONFIG_ID}_field_console { width: 80px; height: 24px; font-size: 14px; text-align: center; } #${CONFIG_ID}_buttons_holder { display: flex; flex-direction: column; } #${CONFIG_ID} .saveclose_buttons { margin: 1px; padding: 4px 0; } #${CONFIG_ID} .reset_holder { padding-top: 4px; } `; const CONFIG_IFRAME_CSS = css` position: fixed; z-index: 99999; width: 300px; height: 400px; border: 1px solid; border-radius: 10px; `; const CONFIG_FIELDS = { cookies: { label: "Try Pass Cookies", type: "select", options: ["yes", "no"], default: "no", }, profile: { label: "MPV Profile", type: "text", default: "default", }, quality: { label: "Prefer Video Quality", type: "select", options: ["default", "2160p", "1440p", "1080p", "720p", "480p", "360p"], default: "default", }, v_codec: { label: "Prefer Video Codec", type: "select", options: ["default", "av01", "vp9", "h265", "h264"], default: "default", }, console: { label: "Run With Console", type: "select", options: ["yes", "no"], default: "yes", }, }; // GM_config init GM_config.init({ id: CONFIG_ID, title: GM_info.script.name, fields: CONFIG_FIELDS, events: { init: () => { let quality = GM_config.get("quality").toLowerCase(); let v_codec = GM_config.get("v_codec").toLowerCase(); if (!CONFIG_FIELDS.quality.options.includes(quality)) { GM_config.set("quality", "default"); } if (!CONFIG_FIELDS.v_codec.options.includes(v_codec)) { GM_config.set("v_codec", "default"); } }, save: () => { let profile = GM_config.get("profile").trim(); if (profile === "") { GM_config.set("profile", "default"); } else { GM_config.set("profile", profile); } updateButton(); GM_config.close(); }, reset: () => { GM_config.save(); }, }, css: CONFIG_CSS.trim(), }); // URL-safe base64 encode function btoaUrl(url) { return btoa(url).replace(/\//g, "_").replace(/\+/g, "-").replace(/\=/g, ""); } // Generate protocol function generateProto(url) { let cookies = GM_config.get("cookies").toLowerCase(); let profile = GM_config.get("profile").trim(); let quality = GM_config.get("quality").toLowerCase(); let v_codec = GM_config.get("v_codec").toLowerCase(); let console = GM_config.get("console").toLowerCase(); let options = []; let proto; if (console === "yes") { proto = "mpv-debug://play/" + btoaUrl(url); } else { proto = "mpv://play/" + btoaUrl(url); } if (cookies === "yes") { options.push("cookies=" + document.location.hostname + ".txt"); } if (profile !== "default" && profile !== "") { options.push("profile=" + profile); } if (quality !== "default") { options.push("quality=" + quality); } if (v_codec !== "default") { options.push("v_codec=" + v_codec); } if (options.length !== 0) { proto += "/?"; options.forEach((option, index) => { proto += option; if (index + 1 !== options.length) { proto += "&"; } }); } return proto; } // Check the URL is matched or not function matchUrl() { if (MATCHERS[location.hostname]) { let site = MATCHERS[location.hostname]; let path = location.pathname; for (const item of site.list) { if (path.startsWith(item)) { if ( path.charAt(item.length) === "/" || path.charAt(item.length) === "" ) { return site.mode; } } } if (path !== "/") { return !site.mode; } return false; } } // Update button display status and URL function updateButton() { let isMatch = matchUrl(); let button = document.getElementsByClassName("pwm-play")[0]; if (button) { button.style = isMatch && !document.fullscreenElement ? "display: block" : "display: none"; button.href = isMatch ? generateProto(location.href) : ""; } } // Notify update about mpv-handler function notifyUpdate() { let version = GM_getValue("mpvHandlerVersion", null); if (version !== MPV_HANDLER_VERSION) { const UPDATE_NOTIFY = { title: `${GM_info.script.name}`, text: `mpv-handler is upgraded to ${MPV_HANDLER_VERSION}\n\nClick to check updates`, onclick: () => { window.open("https://github.com/akiirui/mpv-handler/releases/latest"); }, }; GM_notification(UPDATE_NOTIFY); GM_setValue("mpvHandlerVersion", MPV_HANDLER_VERSION); } } // Add play and settings buttons to page function createButton() { let head = document.getElementsByTagName("head")[0]; let style = document.createElement("style"); if (head) { style.textContent = MPV_CSS.trim(); head.appendChild(style); } let body = document.body; let buttonDiv = document.createElement("div"); let buttonPlay = document.createElement("a"); let buttonSettings = document.createElement("button"); let pauseVideo = (e) => { let videoElement = document.getElementsByTagName("video")[0]; if (videoElement) { videoElement.pause(); } else { setTimeout(pauseVideo, 500, e); } if (e.stopPropagation) e.stopPropagation(); }; if (body) { buttonPlay.className = "pwm-play"; buttonPlay.style = "display: none"; buttonPlay.addEventListener("click", pauseVideo); buttonSettings.className = "pwm-settings"; buttonSettings.addEventListener("click", () => { if (!GM_config.isOpen) { GM_config.open(); GM_config.frame.style = CONFIG_IFRAME_CSS.trim(); } }); buttonDiv.className = "play-with-mpv"; buttonDiv.appendChild(buttonPlay); buttonDiv.appendChild(buttonSettings); body.appendChild(buttonDiv); document.addEventListener("fullscreenchange", () => { let button = document.getElementsByClassName("pwm-play")[0]; button.style = document.fullscreenElement ? "display: none" : "display: block"; }); } } // Detect PJAX changes function detectPJAX() { let previousUrl = null; let currentUrl = null; setInterval(() => { currentUrl = location.href; if (previousUrl !== currentUrl) { updateButton(); previousUrl = currentUrl; } }, 500); } // Fix TrustedHTML if (window.trustedTypes && window.trustedTypes.createPolicy) { window.trustedTypes.createPolicy("default", { createHTML: (string) => string, }); } notifyUpdate(); createButton(); detectPJAX();