// ==UserScript==
// @name MPV Shim Local Connection
// @version 1
// @grant GM.xmlHttpRequest
// @include https://app.plex.tv/*
// @connect 127.0.0.1
// @description Allow Plex to connect to MPV Shim running on the same computer without a local Plex server.
// @namespace https://greasyfork.org/users/456605
// ==/UserScript==
function messageHandler(event) {
let message;
try {
message = JSON.parse(event.data);
} catch(_) {
return;
}
if (message.eventName != "gm_xhr_send") return;
let parsedURL = new URL(message.url);
parsedURL.host = "127.0.0.1:3000";
parsedURL.protocol = "http:";
GM.xmlHttpRequest({
method: 'GET',
url: parsedURL.toString(),
headers: {
"X-Plex-Client-Identifier": parsedURL.searchParams.get("X-Plex-Client-Identifier")
},
onload: function (result) {
window.postMessage(JSON.stringify({
eventName: "gm_xhr_recv",
response: result.responseText,
headers: result.responseHeaders,
id: message.id
}), "*");
}
});
}
window.addEventListener("message", messageHandler, false);
function main () {
// From https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
let clientId = localStorage.getItem('gmpx_uuid');
if (!clientId) {
clientId = uuidv4();
localStorage.setItem('gmpx_uuid', clientId);
}
window.gmpx_eventHandlers = {};
window.gmpx_id = 0;
const parser = new DOMParser();
const serializer = new XMLSerializer();
function gmpx_messageHandler(event) {
let message;
try {
message = JSON.parse(event.data);
} catch(_) {
return;
}
if (message.eventName != "gm_xhr_recv") return;
window.gmpx_eventHandlers[message.id](message);
window.gmpx_eventHandlers[message.id] = undefined;
}
window.addEventListener("message", gmpx_messageHandler, false);
function intercept(url, responseText) {
if (url == "") return;
let parsedURL = new URL(url);
if (parsedURL.pathname == "/clients") {
const xml = parser.parseFromString(responseText, "text/xml");
const s = xml.createElement("Server")
s.setAttribute("name", "local (direct)");
s.setAttribute("host", "127.0.0.1");
s.setAttribute("address", "127.0.0.1");
s.setAttribute("port", "3000");
s.setAttribute("machineIdentifier", clientId);
s.setAttribute("version", "1.0");
s.setAttribute("protocol", "plex");
s.setAttribute("product", "Plex MPV Shim");
s.setAttribute("deviceClass", "pc");
s.setAttribute("protocolVersion", "1");
s.setAttribute("protocolCapabilities", "timeline,playback,navigation,playqueues");
xml.children[0].appendChild(s);
return serializer.serializeToString(xml);
} else {
return responseText;
}
}
// From https://stackoverflow.com/questions/26447335/
// Please note: This is very dirty in the way it works. Don't expect it to work perfectly in all areas.
(function() {
// create XMLHttpRequest proxy object
var oldXMLHttpRequest = XMLHttpRequest;
// define constructor for my proxy object
XMLHttpRequest = function() {
var actual = new oldXMLHttpRequest();
var self = this;
self.override = false;
this.onreadystatechange = null;
// this is the actual handler on the real XMLHttpRequest object
actual.onreadystatechange = function() {
if (this.readyState == 4 && (actual.responseType == '' || actual.responseType == 'text')) {
try {
self.responseText = intercept(actual.responseURL, actual.responseText);
} catch (err) {
self.responseText = actual.responseText;
}
}
if (self.onreadystatechange) {
return self.onreadystatechange();
}
};
// add all proxy getters/setters
["response", "upload", "ontimeout, timeout", "withCredentials", "onerror", "onprogress"].forEach(function(item) {
Object.defineProperty(self, item, {
get: function() { return actual[item];},
set: function(val) { actual[item] = val;}
});
});
// add all proxy getters/setters
["statusText", "status", "readyState", "responseURL", "responseType"].forEach(function(item) {
Object.defineProperty(self, item, {
get: function() {
if (self.hasOwnProperty("_" + item)) {
return self["_" + item];
} else {
return actual[item];
}
},
set: function(val) {actual[item] = val;}
});
});
// add all pure proxy pass-through methods
["addEventListener", "abort", "getResponseHeader", "overrideMimeType", "setRequestHeader"].forEach(function(item) {
Object.defineProperty(self, item, {
value: function() {
if (self.override) { return; }
return actual[item].apply(actual, arguments);
}
});
});
self.open = function() {
if (arguments[0] == "GET" && (new URL(arguments[1])).searchParams.get("X-Plex-Target-Client-Identifier") == clientId) {
const url = arguments[1];
self.override = true;
self._readyState = 1;
self._send = function() {
const id = window.gmpx_id++;
self._responseURL = url;
self._responseType = "";
window.gmpx_eventHandlers[id] = function(result) {
self._readyState = 4;
self._status = 200;
self._statusText = "OK";
self.responseText = result.response;
self.headers = result.headers;
if (self.onreadystatechange) {
self.onreadystatechange();
}
if (self._onload) {
self._onload();
}
};
window.postMessage(JSON.stringify({
eventName: "gm_xhr_send",
url: url,
id: id
}), "*");
}
} else {
return actual.open.apply(actual, arguments);
}
}
self.send = function() {
if (self.override) {
self._send();
} else {
return actual.send.apply(actual, arguments);
}
}
self.getAllResponseHeaders = function() {
if (self.override) {
const headers = self.headers.split("\r\n");
for (let i = 0; i < headers.length; i++) {
if (headers[i].indexOf("x-plex-client-identifier") >= 0) {
headers[i] = "x-plex-client-identifier: " + clientId;
}
}
return headers.join("\r\n");
} else {
return actual.getAllResponseHeaders.apply(actual, arguments);
}
}
Object.defineProperty(self, "responseXML", {
get: function() {
if (self.override) {
return parser.parseFromString(self.responseText, "text/xml");
} else {
return actual[item];
}
}
});
Object.defineProperty(self, "onload", {
get: function() { if (self.override) return self._onload; return actual.onload;},
set: function(val) { if (self.override) self._onload = val; else actual.onload = val;}
});
}
})();
}
// From https://stackoverflow.com/questions/2303147/
var script = document.createElement('script');
script.appendChild(document.createTextNode('('+ main +')();'));
(document.body || document.head || document.documentElement).appendChild(script);