Greasy Fork 支持简体中文。

MPV Shim Local Connection

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

目前為 2020-04-01 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name MPV Shim Local Connection
  3. // @version 2
  4. // @grant GM.xmlHttpRequest
  5. // @include https://app.plex.tv/*
  6. // @connect 127.0.0.1
  7. // @description Allow Plex to connect to MPV Shim running on the same computer without a local Plex server.
  8. // @namespace https://greasyfork.org/users/456605
  9. // ==/UserScript==
  10.  
  11. function messageHandler(event) {
  12. let message;
  13. try {
  14. message = JSON.parse(event.data);
  15. } catch(_) {
  16. return;
  17. }
  18. if (message.eventName != "gm_xhr_send") return;
  19. let parsedURL = new URL(message.url);
  20. parsedURL.host = "127.0.0.1:3000";
  21. parsedURL.protocol = "http:";
  22. GM.xmlHttpRequest({
  23. method: 'GET',
  24. url: parsedURL.toString(),
  25. headers: {
  26. "X-Plex-Client-Identifier": parsedURL.searchParams.get("X-Plex-Client-Identifier")
  27. },
  28. onload: function (result) {
  29. window.postMessage(JSON.stringify({
  30. eventName: "gm_xhr_recv",
  31. response: result.responseText,
  32. headers: result.responseHeaders,
  33. id: message.id
  34. }), "*");
  35. }
  36. });
  37. }
  38.  
  39. window.addEventListener("message", messageHandler, false);
  40.  
  41. function main () {
  42. // From https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
  43. function uuidv4() {
  44. return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
  45. (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  46. );
  47. }
  48. let clientId = localStorage.getItem('gmpx_uuid');
  49. if (!clientId) {
  50. clientId = uuidv4();
  51. localStorage.setItem('gmpx_uuid', clientId);
  52. }
  53. let serverId = localStorage.getItem('gmpx_suuid');
  54. if (!serverId) {
  55. serverId = uuidv4();
  56. localStorage.setItem('gmpx_suuid', serverId);
  57. }
  58.  
  59. // Yes I know this is disgusting. But apparently you can't cast to a server that isn't local.
  60. // The Plex Web App doesn't even *try* to check for clients.
  61. var inject = true;
  62. var fake_cast_server_resource = {
  63. "name": "fake-cast-server",
  64. "product": "Plex Media Server",
  65. "productVersion": "1.18.9.2571-e106a8a91",
  66. "platform": "Linux",
  67. "platformVersion": "10 (buster)",
  68. "device": "PC",
  69. "clientIdentifier": serverId,
  70. "createdAt": "2000-01-01T00:00:00Z",
  71. "lastSeenAt": "2000-01-01T00:00:00Z",
  72. "provides": "server",
  73. "ownerId": null,
  74. "sourceTitle": null,
  75. "publicAddress": "0.0.0.0",
  76. "accessToken": "AAAAAAAAAAAAAAAAAAAA",
  77. "owned": false,
  78. "home": false,
  79. "synced": false,
  80. "relay": false,
  81. "presence": false,
  82. "httpsRequired": false,
  83. "publicAddressMatches": false,
  84. "dnsRebindingProtection": false,
  85. "natLoopbackSupported": true,
  86. "connections": [
  87. {
  88. "protocol": "https",
  89. "address": "127.0.0.1",
  90. "port": 32400,
  91. "uri": "https://fake.uri",
  92. "local": true,
  93. "relay": false,
  94. "IPv6": false
  95. }
  96. ]
  97. };
  98.  
  99. var fake_cast_server_provider = {
  100. "MediaContainer": {
  101. "size": 1,
  102. "allowCameraUpload": false,
  103. "allowChannelAccess": false,
  104. "allowMediaDeletion": false,
  105. "allowSharing": false,
  106. "allowSync": false,
  107. "allowTuners": false,
  108. "backgroundProcessing": false,
  109. "certificate": true,
  110. "companionProxy": true,
  111. "countryCode": "usa",
  112. "diagnostics": "",
  113. "eventStream": false,
  114. "friendlyName": "fake-cast-server",
  115. "livetv": 7,
  116. "machineIdentifier": serverId,
  117. "myPlex": false,
  118. "myPlexMappingState": "mapped",
  119. "myPlexSigninState": "ok",
  120. "myPlexSubscription": true,
  121. "myPlexUsername": "admin@fake.uri",
  122. "ownerFeatures": "",
  123. "photoAutoTag": false,
  124. "platform": "Linux",
  125. "platformVersion": "10 (buster)",
  126. "pluginHost": false,
  127. "pushNotifications": false,
  128. "readOnlyLibraries": false,
  129. "streamingBrainABRVersion": 3,
  130. "streamingBrainVersion": 2,
  131. "sync": false,
  132. "transcoderActiveVideoSessions": 0,
  133. "transcoderAudio": false,
  134. "transcoderLyrics": false,
  135. "transcoderSubtitles": false,
  136. "transcoderVideo": false,
  137. "transcoderVideoBitrates": "64,96,208,320,720,1500,2000,3000,4000,8000,10000,12000,20000",
  138. "transcoderVideoQualities": "0,1,2,3,4,5,6,7,8,9,10,11,12",
  139. "transcoderVideoResolutions": "128,128,160,240,320,480,768,720,720,1080,1080,1080,1080",
  140. "updatedAt": 946702800,
  141. "updater": false,
  142. "version": "1.18.9.2571-e106a8a91",
  143. "voiceSearch": false,
  144. "MediaProvider": []
  145. }
  146. };
  147. window.gmpx_eventHandlers = {};
  148. window.gmpx_id = 0;
  149. const parser = new DOMParser();
  150. const serializer = new XMLSerializer();
  151. function gmpx_messageHandler(event) {
  152. let message;
  153. try {
  154. message = JSON.parse(event.data);
  155. } catch(_) {
  156. return;
  157. }
  158. if (message.eventName != "gm_xhr_recv") return;
  159. window.gmpx_eventHandlers[message.id](message);
  160. window.gmpx_eventHandlers[message.id] = undefined;
  161. }
  162. window.addEventListener("message", gmpx_messageHandler, false);
  163. function intercept(url, responseText) {
  164. if (url == "") return;
  165. let parsedURL = new URL(url);
  166. if (parsedURL.pathname == "/clients") {
  167. const xml = parser.parseFromString(responseText, "text/xml");
  168. const s = xml.createElement("Server")
  169.  
  170. s.setAttribute("name", "local (direct)");
  171. s.setAttribute("host", "127.0.0.1");
  172. s.setAttribute("address", "127.0.0.1");
  173. s.setAttribute("port", "3000");
  174. s.setAttribute("machineIdentifier", clientId);
  175. s.setAttribute("version", "1.0");
  176. s.setAttribute("protocol", "plex");
  177. s.setAttribute("product", "Plex MPV Shim");
  178. s.setAttribute("deviceClass", "pc");
  179. s.setAttribute("protocolVersion", "1");
  180. s.setAttribute("protocolCapabilities", "timeline,playback,navigation,playqueues");
  181.  
  182. xml.children[0].appendChild(s);
  183. inject = false;
  184. return serializer.serializeToString(xml);
  185. } else if (parsedURL.pathname == "/api/v2/resources" && parsedURL.hostname == "plex.tv" && inject) {
  186. const parsed = JSON.parse(responseText);
  187. parsed.unshift(fake_cast_server_resource);
  188. return JSON.stringify(parsed);
  189. } else {
  190. return responseText;
  191. }
  192. }
  193.  
  194. // From https://stackoverflow.com/questions/26447335/
  195. // Please note: This is very dirty in the way it works. Don't expect it to work perfectly in all areas.
  196. (function() {
  197. // create XMLHttpRequest proxy object
  198. var oldXMLHttpRequest = XMLHttpRequest;
  199. var oldWebSocket = WebSocket;
  200. WebSocket = function(url, extra) {
  201. var self = this;
  202. if (url.indexOf("fake.uri") >= 0) {
  203. self.override = true;
  204. var actual = {};
  205. } else {
  206. var actual = new oldWebSocket(url, extra);
  207. }
  208. // add all proxy getters/setters
  209. ["binaryType", "bufferedAmount", "extensions", "onclose", "onerror",
  210. "onmessage", "onopen", "protocol", "readyState", "url"].forEach(function(item) {
  211. Object.defineProperty(self, item, {
  212. get: function() { return actual[item];},
  213. set: function(val) { actual[item] = val;}
  214. });
  215. });
  216. // add all pure proxy pass-through methods
  217. ["close", "send"].forEach(function(item) {
  218. Object.defineProperty(self, item, {
  219. value: function() {
  220. if (self.override) { return; }
  221. return actual[item].apply(actual, arguments);
  222. }
  223. });
  224. });
  225. }
  226. WebSocket.CONNECTING = 0;
  227. WebSocket.OPEN = 1;
  228. WebSocket.CLOSING = 2;
  229. WebSocket.CLOSED = 3;
  230.  
  231. // define constructor for my proxy object
  232. XMLHttpRequest = function() {
  233. var actual = new oldXMLHttpRequest();
  234. var self = this;
  235. self.override = false;
  236.  
  237. this.onreadystatechange = null;
  238.  
  239. // this is the actual handler on the real XMLHttpRequest object
  240. actual.onreadystatechange = function() {
  241. if (this.readyState == 4 && (actual.responseType == '' || actual.responseType == 'text')) {
  242. try {
  243. self.responseText = intercept(actual.responseURL, actual.responseText);
  244. } catch (err) {
  245. self.responseText = actual.responseText;
  246. }
  247. }
  248. if (self.onreadystatechange) {
  249. return self.onreadystatechange();
  250. }
  251. };
  252.  
  253. // add all proxy getters/setters
  254. ["upload", "ontimeout, timeout", "withCredentials", "onerror", "onprogress"].forEach(function(item) {
  255. Object.defineProperty(self, item, {
  256. get: function() { return actual[item];},
  257. set: function(val) { actual[item] = val;}
  258. });
  259. });
  260.  
  261. // add all proxy getters/setters
  262. ["response", "statusText", "status", "readyState", "responseURL", "responseType"].forEach(function(item) {
  263. Object.defineProperty(self, item, {
  264. get: function() {
  265. if (self.hasOwnProperty("_" + item)) {
  266. return self["_" + item];
  267. } else {
  268. return actual[item];
  269. }
  270. },
  271. set: function(val) {actual[item] = val;}
  272. });
  273. });
  274.  
  275. // add all pure proxy pass-through methods
  276. ["addEventListener", "abort", "getResponseHeader", "overrideMimeType", "setRequestHeader"].forEach(function(item) {
  277. Object.defineProperty(self, item, {
  278. value: function() {
  279. if (self.override) { return; }
  280. return actual[item].apply(actual, arguments);
  281. }
  282. });
  283. });
  284.  
  285. self.open = function() {
  286. if (arguments[0] == "GET") {
  287. const parsedURL = new URL(arguments[1]);
  288. if (parsedURL.searchParams.get("X-Plex-Target-Client-Identifier") == clientId) {
  289. const url = arguments[1];
  290. self.override = true;
  291. self._readyState = 1;
  292. self._send = function() {
  293. const id = window.gmpx_id++;
  294. self._responseURL = url;
  295. self._responseType = "";
  296. window.gmpx_eventHandlers[id] = function(result) {
  297. self._readyState = 4;
  298. self._status = 200;
  299. self._statusText = "OK";
  300. self.responseText = result.response;
  301. self.headers = result.headers;
  302. if (self.onreadystatechange) {
  303. self.onreadystatechange();
  304. }
  305. if (self._onload) {
  306. self._onload();
  307. }
  308. };
  309. window.postMessage(JSON.stringify({
  310. eventName: "gm_xhr_send",
  311. url: url,
  312. id: id
  313. }), "*");
  314. }
  315. } else if (parsedURL.hostname == "fake.uri") {
  316. const url = arguments[1];
  317. self.override = true;
  318. self.override2 = true;
  319. self._readyState = 1;
  320. self._send = function() {
  321. self._responseURL = url;
  322. self._responseType = "";
  323. self._status = 200;
  324. self._statusText = "OK";
  325. self._readyState = 4;
  326. self.responseText = "";
  327. if (parsedURL.pathname == "/clients") {
  328. const xml = parser.parseFromString('<MediaContainer/>', 'text/xml');
  329. const s = xml.createElement("Server");
  330. s.setAttribute("name", "local (direct)");
  331. s.setAttribute("host", "127.0.0.1");
  332. s.setAttribute("address", "127.0.0.1");
  333. s.setAttribute("port", "3000");
  334. s.setAttribute("machineIdentifier", clientId);
  335. s.setAttribute("version", "1.0");
  336. s.setAttribute("protocol", "plex");
  337. s.setAttribute("product", "Plex MPV Shim");
  338. s.setAttribute("deviceClass", "pc");
  339. s.setAttribute("protocolVersion", "1");
  340. s.setAttribute("protocolCapabilities", "timeline,playback,navigation,playqueues");
  341. xml.children[0].appendChild(s);
  342. self.responseText = serializer.serializeToString(xml);
  343. } else if (parsedURL.pathname == "/neighborhood/devices") {
  344. return "<MediaContainer size=\"0\"/>";
  345. } else if (parsedURL.pathname == "/media/providers") {
  346. self.responseText = JSON.stringify(fake_cast_server_provider);
  347. } else if (parsedURL.pathname == "/player/proxy/poll") {
  348. return;
  349. } else {
  350. console.log("Unhandled URL: " + arguments[1]);
  351. }
  352.  
  353. if (self.onreadystatechange) {
  354. self.onreadystatechange();
  355. }
  356. if (self._onload) {
  357. self._onload();
  358. }
  359. }
  360. } else {
  361. return actual.open.apply(actual, arguments);
  362. }
  363. } else {
  364. return actual.open.apply(actual, arguments);
  365. }
  366. }
  367.  
  368. self.send = function() {
  369. if (self.override) {
  370. self._send();
  371. } else {
  372. return actual.send.apply(actual, arguments);
  373. }
  374. }
  375.  
  376. self.getAllResponseHeaders = function() {
  377. if (self.override2) {
  378. return "";
  379. } else if (self.override) {
  380. const headers = self.headers.split("\r\n");
  381. for (let i = 0; i < headers.length; i++) {
  382. if (headers[i].indexOf("x-plex-client-identifier") >= 0) {
  383. headers[i] = "x-plex-client-identifier: " + clientId;
  384. }
  385. }
  386. return headers.join("\r\n");
  387. } else {
  388. return actual.getAllResponseHeaders.apply(actual, arguments);
  389. }
  390.  
  391. }
  392.  
  393. Object.defineProperty(self, "responseXML", {
  394. get: function() {
  395. if (self.override) {
  396. return parser.parseFromString(self.responseText, "text/xml");
  397. } else {
  398. return actual[item];
  399. }
  400. }
  401. });
  402.  
  403. Object.defineProperty(self, "onload", {
  404. get: function() { if (self.override) return self._onload; return actual.onload;},
  405. set: function(val) { if (self.override) self._onload = val; else actual.onload = val;}
  406. });
  407. }
  408. })();
  409. }
  410.  
  411. // From https://stackoverflow.com/questions/2303147/
  412. var script = document.createElement('script');
  413. script.appendChild(document.createTextNode('('+ main +')();'));
  414. (document.body || document.head || document.documentElement).appendChild(script);