MPV Shim Local Connection

Allow Plex to connect to MPV Shim running on the same computer without a local Plex server.

目前为 2020-03-16 提交的版本。查看 最新版本

// ==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);