// ==UserScript==
// @name Open with MPV
// @version 1.0.7
// @author Ranko
// @description Open video/stream with MPV
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @match *://*/*
// @run-at document-start
// @run-at context-menu
// ==/UserScript==
(function() {
'use strict';
let config_id = 'open-with-mpv';
const match_url = [
/www.youtube.com\/(?:watch|playlist)/,
/twitch.tv\/.+/,
/www.douyu.com\/.+/,
/www.bilibili.com\/video/,
/live.bilibili.com\/.+/,
];
let frame_css = `
position: fixed;
z-index: 99999;
width: 360px;
height: 280px;
border: 1px solid;
`;
let config_css = `
body {
display: flex;
justify-content: center;
}
#${config_id} .config_header {
color: color-mix(in srgb, currentColor 13%, -moz-Dialog) !important;
}
#${config_id}_resetLink {
color: color-mix(in srgb, currentColor 13%, -moz-Dialog) !important;
}
#${config_id} .field_label {
display: inline-block;
width: 120px;
color: color-mix(in srgb, currentColor 13%, -moz-Dialog) !important;
}
#${config_id}_field_cookies {
width: 150px;
}
#${config_id}_field_quality,
#${config_id}_field_v_codec,
#${config_id}_field_st_quality {
width: 90px;
}
`;
GM_config.init({
id: config_id,
title: GM_info.script.name + ' Settings',
css: config_css.trim(),
fields: {
referer: {
label: 'Pass Referer',
type: 'checkbox',
default: false,
},
cookies: {
label: 'Youtube Cookies',
type: 'text',
default: '',
},
quality: {
label: 'Youtube Quality',
type: 'select',
options: ["default", "2160p", "1440p", "1080p", "720p", "480p", "360p"],
default: 'default',
},
v_codec: {
label: 'Youtube Codec',
type: 'select',
options: ["default", "av01", "vp9", "h265", "h264"],
default: 'default',
},
st_quality: {
label: 'Streamlink Quality',
type: 'select',
options: ["best", "1080p", "720p", "480p", "360p", "worst"],
default: 'best',
},
open_chat: {
label: 'Open chat',
type: 'checkbox',
default: false,
}
},
events: {
save: () => {
let cookies = GM_config.get("cookies").trim();
if (cookies === '') {
GM_config.set("cookies", '');
} else {
GM_config.set("cookies", cookies);
}
}
}
})
function btoaUrl(url) {
return btoa(url).replace(/\//g, "_").replace(/\+/g, "-").replace(/\=/g, "");
}
function genMPVLink(url, type) {
let referer = GM_config.get("referer");
let cookies = GM_config.get("cookies").trim();
let quality = GM_config.get("quality").toLowerCase();
let v_codec = GM_config.get("v_codec").toLowerCase();
let st_quality = GM_config.get("st_quality").toLowerCase();
let proto = 'mpv://' + type + '/' + btoaUrl(url);
let options = [];
if (referer) {
options.push("referer=" + btoaUrl(location.href));
}
if (type == 'play') {
if (cookies !== "") {
options.push("cookies=" + cookies);
}
if (quality !== "default") {
options.push("quality=" + quality);
}
if (v_codec !== "default") {
options.push("v_codec=" + v_codec);
}
} else if (type == 'stream') {
options.push("quality=" + st_quality);
} else if (type == 'pipe') {
//
}
if (options.length !== 0) {
proto += "/?";
options.forEach((option, index) => {
proto += option;
if (index + 1 !== options.length) {
proto += "&";
}
});
}
return proto;
}
function matchUrl(url) {
return match_url.some(regex => regex.test(url));
}
function getParentByTagName(e, tagName) {
tagName = tagName.toLowerCase();
if (e.tagName.toLowerCase() == tagName) {
return e;
}
while (e && e.parentNode) {
e = e.parentNode;
if (e.tagName && e.tagName.toLowerCase() == tagName) {
return e;
}
}
return "undefined";
}
function openHandle(type) {
var l = GM_getValue("URL", null);
console.log("link:" + l);
var mlink = null;
if (l !== null) {
let open_chat = GM_config.get("open_chat");
if (open_chat) { livechatOpen(l); }
mlink = genMPVLink(l, type);
GM_setValue("URL", null);
console.log(mlink);
location.href = mlink;
}
}
function openMPV() {
openHandle('play');
}
function openStreamlink() {
openHandle('stream');
}
function openPip() {
openHandle('pipe');
}
function getLink(e) {
if (e.buttons == 2) {
var l = null;
var target = getParentByTagName(e.target, "A").href;
console.log(document.href);
if(matchUrl(target)) {
l = target;
} else if (matchUrl(location.href)) {
l = location.href;
}
if (l !== null) {
//Remove param dyshci in douyu
var cr_url = new URL(l);
if (cr_url.href.indexOf('www.douyu.com')) {
cr_url.searchParams.delete('dyshci');
l = cr_url.href;
}
console.log(l);
}
GM_setValue("URL", l);
}
}
function getYouTubeVideoId(url) {
const regex_id = /[?&]v=([^#&?]*).*/;
const match = url.match(regex_id);
return match ? match[1] : null;
}
function checkYouTubeLiveStatus(url) {
const videoId = getYouTubeVideoId(url);
return new Promise((resolve, reject) => {
const apiKey = 'AIzaSyBlCwCvXzxf2gpD5sHJRKhgJs7FOTeaXsg';
const apiUrl = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${apiKey}&part=liveStreamingDetails`;
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
const liveDetails = data.items[0].liveStreamingDetails;
const isLiveStream = liveDetails && liveDetails.concurrentViewers;
resolve(isLiveStream);
} catch (error) {
reject(error);
}
},
onerror: function(error) {
reject(error);
}
});
});
}
function popoutChat(url) {
window.open(url, "", "fullscreen=no,toolbar=no,titlebar=no,menubar=no,location=no,width=400,height=600");
}
function livechatOpen(url) {
var cur_url = new URL(url);
if (cur_url.href.indexOf('youtube.com/watch') != -1) {
checkYouTubeLiveStatus(cur_url.href)
.then(isLiveStream => {
if (isLiveStream !== undefined) {
console.log('Is live stream:', isLiveStream);
popoutChat("https://www.youtube.com/live_chat?is_popout=1&v=" + cur_url.search.split("v=")[1]);
}
})
.catch(error => {
console.error('Error:', error);
});
} else if (cur_url.href.match('https://.*?.twitch.tv/.')) {
popoutChat("https://www.twitch.tv/popout" + cur_url.pathname + "/chat?popout=");
}
}
document.addEventListener('mousedown', getLink, false);
GM_registerMenuCommand("Open with MPV", openMPV);
GM_registerMenuCommand("Open with Streamlink", openStreamlink);
GM_registerMenuCommand("Open with Pipe", openPip);
GM_registerMenuCommand('Settings', () => {
GM_config.open();
GM_config.frame.style = frame_css.trim();
});
})();