Plex External Player PotPlayer

插件用于激活本地PotPlayer 播放器使用。

当前为 2021-07-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Plex External Player PotPlayer
  3. // @namespace https://github.com/Tosslog/PlexMediaServer/tree/main/Web%20Plugin/Plex%20External%20Player%20PotPlayer
  4. // @version 1.1.0
  5. // @description 插件用于激活本地PotPlayer 播放器使用。
  6. // @author 北京土著 30344386@qq.com
  7. // @include /^https?://.*:32400/web.*
  8. // @include http://*:32400/web/index.html*
  9. // @include https://*:32400/web/index.html*
  10. // @include https://app.plex.tv/*
  11. // @require http://code.jquery.com/jquery-3.2.1.min.js
  12. // @connect *
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js
  14. // @grant GM_xmlhttpRequest
  15. // ==/UserScript==
  16.  
  17. $("head").append(
  18. '<link href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css" rel="stylesheet" type="text/css">'
  19. );
  20.  
  21. // 消息设定
  22. toastr.options = {
  23. "closeButton": true,
  24. "debug": false,
  25. "newestOnTop": true,
  26. "progressBar": true,
  27. "positionClass": "toast-bottom-right",
  28. "preventDuplicates": false,
  29. "onclick": null,
  30. "showDuration": "300",
  31. "hideDuration": "1000",
  32. "timeOut": "5000",
  33. "extendedTimeOut": "5000",
  34. "showEasing": "swing",
  35. "hideEasing": "linear",
  36. "showMethod": "fadeIn",
  37. "hideMethod": "fadeOut"
  38. };
  39.  
  40. // 输出消息
  41. var showToast = function (msg, error) {
  42. var title = 'Plex External PotPlayer';
  43. if (error) {
  44. toastr.error(msg, title, { timeOut: 10000 });
  45. logMessage(msg, 'Error');
  46. }
  47. else {
  48. toastr.success(msg, title);
  49. }
  50. };
  51.  
  52. // 控制台输出
  53. var logMessage = function (msg, _type) {
  54. console.log('[Plex External PotPlayer ' + _type + ' ] ' + msg);
  55. };
  56.  
  57. //得到 Plex Servers
  58. var makeRequest = function (url, serverId) {
  59. return new Promise(function (resolve, reject) {
  60. var origAccessToken = localStorage.myPlexAccessToken;
  61. var serverNode = {};
  62. if (localStorage.users) {
  63. serverNode = JSON.parse(localStorage.users);
  64. //console.log(serverNode);
  65. } else {
  66. logMessage('未找到用户详细信息', 'Error');
  67. }
  68. var tokenToTry = origAccessToken;
  69. if (serverNode === undefined) {
  70. serverNode = {
  71. users: []
  72. };
  73. }
  74. // logMessage('寻找 serverId 的令牌:' + serverId)
  75. let tokenFound = false
  76. if (serverId !== undefined) {
  77. serverLoop:
  78. for (var i = 0; i < serverNode.users.length; i++) {
  79. // logMessage('Checking server list (' + serverNode.users[i].servers.length + ') for user:' + serverNode.users[i].username)
  80. for (var j = 0; j < serverNode.users[i].servers.length; j++) {
  81. // logMessage('Checking server with id ' + serverNode.users[i].servers[j].machineIdentifier)
  82. if (serverNode.users[i].servers[j].machineIdentifier == serverId) {
  83. tokenToTry = serverNode.users[i].servers[j].accessToken;
  84. // logMessage('Token found:' + tokenToTry);
  85. tokenFound = true
  86. break serverLoop;
  87. }
  88. }
  89. }
  90. token = tokenToTry;
  91. if (!tokenFound) {
  92. showToast('找不到身份验证信息', 1);
  93. reject();
  94. return;
  95. }
  96. }
  97. var authedUrl = url + '&X-Plex-Token=' + tokenToTry;
  98. // logMessage('调用 ' + authedUrl, 'Log');
  99. GM_xmlhttpRequest({
  100. method: "GET",
  101.  
  102. url: authedUrl,
  103. onload: function (state) {
  104. if (state.status === 200) {
  105. //logMessage('成功调用' + url, 'Log');
  106. resolve(state);
  107. }
  108. },
  109. onreadystatechange: function (state) {
  110. if (state.readyState === 4) {
  111.  
  112. if (state.status === 401) {
  113. logMessage('未授权 ' + url, 'Error');
  114. } else if (state.status !== 200) {
  115. logMessage('请求返回 ' + state.status, 'Log');
  116. showToast('Error calling: ' + url + '. Response: ' + state.responseText + ' Code:' + state.status + ' Message: ' + state.statusText, 1);
  117. }
  118. }
  119. },
  120. });
  121. });
  122. };
  123.  
  124. var getHosts = function () {
  125. makeRequest('https://plex.tv/api/resources?includeHttps=1&X-Plex-Token=' + localStorage.myPlexAccessToken)
  126. .then(function (response) {
  127. //console.log(response)
  128. let parts = response.responseXML.getElementsByTagName('Device');
  129. for (let i = 0; i < parts.length; i++) {
  130. if (parts[i].getAttribute('product') == 'Plex Media Server') {
  131. let connections = parts[i].getElementsByTagName('Connection');
  132. for (let j = 0; j < connections.length; j++) {
  133. if (connections[j].getAttribute('local') == parts[i].getAttribute('publicAddressMatches')) {
  134. pmsUrls.set(parts[i].getAttribute('clientIdentifier'), 'http://' + connections[j].getAttribute('address') + ':' + connections[j].getAttribute('port'));
  135. break;
  136. }
  137. }
  138. }
  139. }
  140. }).catch(function () {
  141. showToast('Failed to get PMS URLs', 1);
  142. });
  143. }
  144.  
  145. var pmsUrls = new Map();
  146. var token = '';
  147. var title = '';
  148. setTimeout(function () {
  149. getHosts();
  150. //console.log(token)
  151. }, 1000);
  152.  
  153.  
  154. // 单击侦听器
  155. var clickListener = function (e) {
  156. e.preventDefault();
  157. e.stopPropagation();
  158. var a = jQuery(e.target).closest('a');
  159. var link = a.attr('href');
  160. var url = link;
  161. if (link === '#' || link === undefined || link === 'javascript:void(0)') {
  162. url = window.location.hash;
  163. }
  164.  
  165. if (url.indexOf('/server/') > -1) {
  166. var serverId = url.split('/')[2];
  167. }
  168.  
  169. if (url.indexOf('%2Fmetadata%2F') > -1) {
  170. var idx = url.indexOf('%2Fmetadata%2F');
  171. var mediaId = url.substr(idx + 14);
  172. var idToken = mediaId.indexOf('&');
  173. if (idToken > -1) {
  174. mediaId = mediaId.substr(0, idToken);
  175. }
  176. }
  177.  
  178. var metaDataPath = pmsUrls.get(serverId) + '/library/metadata/' + mediaId + '?includeConcerts=1&includeExtras=1&includeOnDeck=1&includePopularLeaves=1&includePreferences=1&includeChapters=1&asyncCheckFiles=0&asyncRefreshAnalysis=0&asyncRefreshLocalMediaAgent=0&X-Plex-Token=' + localStorage.myPlexAccessToken;
  179. //console.log('metaDataPath = ' + metaDataPath)
  180. makeRequest(metaDataPath, serverId)
  181. .then((response) => {
  182. let mediaurl = '';
  183. let subtitlelist = [];
  184. let Video = response.responseXML.getElementsByTagName('Video');
  185. for (let i = 0; i < Video.length; i++) {
  186. if (Video[i].attributes['title'] !== undefined) {
  187. title = ' [' + Video[i].attributes['title'].value + '] ';
  188. break;
  189. }
  190. }
  191.  
  192.  
  193. let parts = response.responseXML.getElementsByTagName('Part');
  194. for (let i = 0; i < parts.length; i++) {
  195. if (parts[i].attributes['file'] !== undefined) {
  196. mediaurl = pmsUrls.get(serverId) + parts[i].attributes['key'].value + '?download=1';
  197. }
  198. }
  199.  
  200. let Stream = response.responseXML.getElementsByTagName('Stream');
  201. //console.log(Stream)
  202. for (let i = 0; i < Stream.length; i++) {
  203. if (Stream[i].attributes['key'] !== undefined) {
  204. let subtitlekey = Stream[i].attributes['key'].value;
  205. if (subtitlekey !== undefined) {
  206. subtitlelist.push(subtitlekey);
  207. }
  208. else {
  209. logMessage('未找到本地外挂字幕文件', 'Error');
  210. }
  211. }
  212. }
  213.  
  214. let subtitleUrl = [];
  215. for (let i = 0; i < subtitlelist.length; i++) {
  216. subtitleUrl.push(pmsUrls.get(serverId) + subtitlelist[i]);
  217. };
  218.  
  219. var authedUrl = mediaurl + '&X-Plex-Token=' + token;
  220. let poturl = "potplayer://" + authedUrl + " /sub=" + subtitleUrl[0] + '?X-Plex-Token=' + token;
  221. //console.log(poturl);
  222. showToast('成功解析影片 ' + title + ' 路径,正在激活本地PotPlayer播放器。', 0)
  223. window.open(poturl, "_blank");
  224. });
  225.  
  226. };
  227.  
  228. // 绑定按钮
  229. var bindClicks = function () {
  230. var hasBtn = false;
  231. var toolBar = jQuery("#plex-icon-toolbar-play-560").parent().parent();
  232. toolBar.children('button').each(function (i, e) {
  233. if (jQuery(e).hasClass('plexextplayer'))
  234. hasBtn = true;
  235. });
  236.  
  237.  
  238. if (!hasBtn) {
  239. var template = jQuery('<button class="play-btn media-poster-btn btn-link plexextplayer" tabindex="-1" title="外部播放器"><i class="glyphicon play plexextplayer plexextplayerico"></i></button>');
  240. toolBar.prepend(template);
  241. template.click(clickListener);
  242.  
  243. }
  244.  
  245. // Cover page
  246. jQuery('[class^=MetadataPosterCardOverlay-link]').each(function (i, e) {
  247. e = jQuery(e);
  248. let poster = e.parent();
  249. if (poster.length === 1 && poster[0].className.trim().startsWith('MetadataPosterCardOverlay')) {
  250. let existingButton = poster.find('.plexextplayerico');
  251. if (existingButton.length === 0) {
  252. let url = poster.find('a').attr('href');
  253. let template = jQuery('<a href="' + url + '" aria-haspopup="false" aria-role="button" class="" type="button"><i class="glyphicon play plexextplayer plexextplayerico plexextplayericocover"></i></button>');
  254. let newButton = template.appendTo(poster);
  255. newButton.click(clickListener);
  256. poster.mouseenter(function () {
  257. newButton.find('i').css('display', 'block');
  258. });
  259. poster.mouseleave(function () {
  260. newButton.find('i').css('display', 'none');
  261. });
  262. }
  263. }
  264. });
  265.  
  266. //Thumbnails
  267. jQuery('[class^=PosterCardLink-link]').each(function (i, e) {
  268. e = jQuery(e);
  269. let thumb = e.parent();
  270. if (thumb.length === 1 && thumb[0].className.trim().startsWith('MetadataPosterListItem-card')) {
  271. let existingButton = thumb.find('.plexextplayerico');
  272. if (existingButton.length === 0) {
  273. let url = thumb.find('a').attr('href');
  274. let template = jQuery('<a href="' + url + '" aria-haspopup="false" aria-role="button" class="" type="button"><i class="glyphicon play plexextplayer plexextplayerico plexextplayericocover"></i></button>');
  275. let thumbButton = template.appendTo(thumb);
  276. thumbButton.click(clickListener);
  277. thumb.mouseenter(function () {
  278. thumbButton.find('i').css('display', 'block');
  279. });
  280. thumb.mouseleave(function () {
  281. thumbButton.find('i').css('display', 'none');
  282. });
  283. }
  284. }
  285. });
  286.  
  287. // Playlist
  288. jQuery("span[class^=' MetadataPosterTitle-singleLineTitle']").each(function (i, e) {
  289. e = jQuery(e);
  290. let playlistItem = e.closest("[class^='PlaylistItemRow-overlay']").find("[class^='PlaylistItemMetadata-indexContainer']");
  291. if (playlistItem.length === 1) {
  292. let existingButton = playlistItem.find('.plexextplayerico');
  293. if (existingButton.length === 0) {
  294. let url = e.find('a').attr('href');
  295. let template = jQuery('<a href="' + url + '" aria-haspopup="false" aria-role="button" class="" type="button"><i class="glyphicon play plexextplayer plexextplayerico plexextplayericocover"></i></button>');
  296. let newButton = template.appendTo(playlistItem);
  297. newButton.click(clickListener);
  298. playlistItem.closest("[class^='PlaylistItemRow-overlay']").mouseenter(function () {
  299. newButton.find('i').css('display', 'block');
  300. });
  301. playlistItem.closest("[class^='PlaylistItemRow-overlay']").mouseleave(function () {
  302. newButton.find('i').css('display', 'none');
  303. });
  304. }
  305. }
  306. });
  307. };
  308.  
  309.  
  310. // Make buttons smaller
  311. jQuery('body').append('<style>.plexextplayericocover {right: 10px; top: 10px; position:absolute; display:none;font-size:15px;} .glyphicon.plexfolderextplayerico:before { content: "\\e145"; } .glyphicon.plexextplayerico:before { content: "\\e161"; }</style>');
  312.  
  313.  
  314. // 绑定按钮并每 100 毫秒检查一次新按钮
  315. // 放置脚本最后
  316. setInterval(bindClicks, 100);
  317. bindClicks();