Plex External Player

Play plex videos in an external player

当前为 2017-12-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Plex External Player
  3. // @namespace https://github.com/Kayomani/PlexExternalPlayer
  4. // @version 1.15
  5. // @description Play plex videos in an external player
  6. // @author Kayomani
  7. // @include /^https?://.*:32400/web.*
  8. // @include http://*:32400/web/index.html*
  9. // @require http://code.jquery.com/jquery-3.2.1.min.js
  10. // @connect *
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js
  12. // @grant GM_xmlhttpRequest
  13. // ==/UserScript==
  14.  
  15. $("head").append (
  16. '<link href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css" rel="stylesheet" type="text/css">'
  17. );
  18.  
  19.  
  20. toastr.options = {
  21. "closeButton": true,
  22. "debug": false,
  23. "newestOnTop": true,
  24. "progressBar": true,
  25. "positionClass": "toast-bottom-right",
  26. "preventDuplicates": false,
  27. "onclick": null,
  28. "showDuration": "300",
  29. "hideDuration": "1000",
  30. "timeOut": "5000",
  31. "extendedTimeOut": "1000",
  32. "showEasing": "swing",
  33. "hideEasing": "linear",
  34. "showMethod": "fadeIn",
  35. "hideMethod": "fadeOut"
  36. };
  37.  
  38. var showToast = function(msg, error){
  39. var title = 'Plex External Player';
  40. if(error){
  41. toastr.error(msg, title, {timeOut: 10000});
  42. logMessage(msg);
  43. }
  44. else
  45. {
  46. toastr.success(msg, title);
  47. }
  48. };
  49.  
  50. var logMessage = function(msg){
  51. console.log('[Plex External] ' + msg);
  52. };
  53.  
  54. var makeRequest = function(url, user, server){
  55. return new Promise( function (resolve, reject) {
  56. var origAccessToken = localStorage.myPlexAccessToken;
  57. var serverNode = {};
  58. if(localStorage.users) {
  59. serverNode = JSON.parse(localStorage.users);
  60. } else {
  61. logMessage('User details not found');
  62. }
  63. var tokenToTry = origAccessToken;
  64. if(serverNode===undefined)
  65. {
  66. serverNode = {
  67. users : []
  68. };
  69. }
  70.  
  71. if(user!==undefined && server !==undefined)
  72. {
  73. if(user < serverNode.users.length)
  74. {
  75. if(server < serverNode.users[user].servers.length)
  76. {
  77. tokenToTry = serverNode.users[user].servers[server].accessToken;
  78. }
  79. else
  80. {
  81. showToast('Could not find authentication info', 1);
  82. reject();
  83. return;
  84. }
  85. }
  86. else
  87. {
  88. showToast('Could not find authentication info', 1);
  89. reject();
  90. return;
  91. }
  92. }
  93. var onError = function()
  94. {
  95. if(user===undefined)
  96. {
  97. user = 0;
  98. server = 0;
  99. } else
  100. {
  101. server++;
  102. if(serverNode.users[user].servers.length===server)
  103. {
  104. user++;
  105. server = 0;
  106. }
  107. }
  108. makeRequest(url,user,server).then(resolve, reject);
  109. };
  110. logMessage('Calling ' + url +' clientId ' + localStorage.clientID + 'Token ' + tokenToTry);
  111. GM_xmlhttpRequest({
  112. method: "GET",
  113. headers: headers = {
  114. "X-Plex-Client-Identifier":localStorage.clientID,
  115. "X-Plex-Token":tokenToTry
  116.  
  117. },
  118. url: url,
  119. onload: function(state){
  120. if (state.status === 200) {
  121. logMessage('Called sucessfully to ' + url);
  122. resolve(state);
  123. }
  124. },
  125. onreadystatechange: function(state) {
  126. if (state.readyState === 4) {
  127. if(state.status === 401)
  128. {
  129. logMessage('Not Authorised ' + url);
  130. onError();
  131. } else if (state.status !== 200) {
  132. logMessage('Request returned ' + state.status);
  133. showToast('Error calling: ' + url + '. Response: ' + state.responseText + ' Code:' + state.status + ' Message: ' + state.statusText, 1);
  134. }
  135. }
  136. },
  137. onerror: onError
  138. });
  139. });
  140. };
  141.  
  142.  
  143.  
  144. var markAsPlayedInPlex = function(id) {
  145. logMessage('Marking ' + id + ' as played');
  146. return makeRequest(window.location.origin + '/:/scrobble?key='+ id +'&identifier=com.plexapp.plugins.library').catch(function(){
  147. showToast('Failed to mark item ' + id + ' as played');
  148. });
  149. };
  150.  
  151. var openItemOnAgent = function(path, id, openFolder) {
  152. if(openFolder){
  153. var fwd = path.lastIndexOf('/');
  154. var bck = path.lastIndexOf('\\');
  155. var best = fwd>bck?fwd:bck;
  156. if(best>-1){
  157. path = path.substr(0, best);
  158. }
  159. }
  160. showToast('Playing ' + path, 0);
  161. logMessage('Playing ' + path);
  162. // umicrosharp doesn't handle plus properly
  163. path = path.replace(/\+/g, '[PLEXEXTPLUS]');
  164. var url = 'http://localhost:7251/?protocol=2&item=' + encodeURIComponent(path);
  165. return new Promise(function (resolve, reject) {
  166. makeRequest(url).then(function(){
  167. markAsPlayedInPlex(id).then(resolve, reject);
  168. },reject);
  169. });
  170. };
  171.  
  172. var clickListener = function(e) {
  173. e.preventDefault();
  174. e.stopPropagation();
  175. var a = jQuery(e.target).closest('a');
  176. var link = a.attr('href');
  177. var openFolder = jQuery(e.target).attr('title') === 'Open folder';
  178. var url = link;
  179. if (link === '#' || link === undefined || link === 'javascript:void(0)') {
  180. url = window.location.hash;
  181. }
  182.  
  183. if (url.indexOf('%2Fmetadata%2F') > -1) {
  184. var idx = url.indexOf('%2Fmetadata%2F');
  185. var id = url.substr(idx + 14);
  186.  
  187. // Get metadata
  188. var metaDataPath = window.location.origin + '/library/metadata/' + id + '?checkFiles=1&includeExtras=1';
  189. makeRequest(metaDataPath)
  190. .then(function(response){
  191. // Play the first availible part
  192. var parts = response.responseXML.getElementsByTagName('Part');
  193. for (var i = 0; i < parts.length; i++) {
  194. if (parts[i].attributes['file'] !== undefined) {
  195. openItemOnAgent(parts[i].attributes['file'].value, id, openFolder).catch(function(){
  196. showToast('Failed to connect to agent, is it running or firewalled?',1);
  197. });
  198. return;
  199. }
  200. }
  201.  
  202. if (parts.length === 0) {
  203. // If we got a directory/Season back then get the files in it
  204. var dirs = response.responseXML.getElementsByTagName('Directory');
  205. if (dirs.length > 0) {
  206. makeRequest(window.location.origin + dirs[0].attributes['key'].value)
  207. .then(function(response){
  208. var videos = response.responseXML.getElementsByTagName('Video');
  209. var file = null;
  210. var id = null;
  211. if(videos.length === 0)
  212. {
  213. showToast('Could not determine which video to play as there are multiple seasons.',true);
  214. return;
  215. }
  216. for (var i = 0; i < videos.length; i++) {
  217. var vparts = videos[i].getElementsByTagName('Part');
  218. if (vparts.length > 0) {
  219. file = vparts[0].attributes['file'].value;
  220. id = vparts[0].attributes['id'].value;
  221. if (videos[i].attributes['lastViewedAt'] === null || videos[i].attributes['lastViewedAt'] === undefined) {
  222. break;
  223. }
  224. }
  225. }
  226.  
  227. if (file !== null)
  228. {
  229. openItemOnAgent(file, id, openFolder).catch(function(){
  230. showToast('Failed to connect to agent, is it running or firewalled?',1);
  231. });
  232. }
  233. }).catch(function(){
  234. showToast('Failed to get information for directory',1);
  235. });
  236. }
  237. }
  238. }, function(error){
  239. showToast('Error getting metadata from ' + metaDataPath + "Error: " + error, 1);
  240. logMessage('Error ' + JSON.stringify(error));
  241. });
  242. }
  243. };
  244.  
  245. var bindClicks = function() {
  246. var hasBtn = false;
  247. var toolBar= jQuery(".plex-icon-toolbar-play-560").parent().parent();
  248. toolBar.children('button').each(function(i, e) {
  249. if(jQuery(e).hasClass('plexextplayer'))
  250. hasBtn = true;
  251. });
  252.  
  253.  
  254. if(!hasBtn)
  255. {
  256. var template = jQuery('<button class="play-btn media-poster-btn btn-link plexextplayer" tabindex="-1" title="Play Externally"><i class="glyphicon play plexextplayer plexextplayerico"></i></button><button class="play-btn media-poster-btn btn-link plexextplayer" title="Open folder" tabindex="-1"><i data-type="folder" title="Open folder" class="glyphicon play plexextplayer plexfolderextplayerico"></i></button>');
  257. toolBar.prepend(template);
  258. template.click(clickListener);
  259.  
  260. }
  261.  
  262. // Cover page
  263. jQuery(".plex-icon-more-560").each(function(i, e) {
  264. e = jQuery(e);
  265. var poster = e.parent().parent();
  266. if(poster.length === 1 && poster[0].className.trim().startsWith('MetadataPosterCardOverlay'))
  267. {
  268. var existingButton = poster.find('.plexextplayerico');
  269. if(existingButton.length === 0)
  270. {
  271. var url = poster.find('a').attr('href');
  272. var template = jQuery('<a href="'+ url +'" aria-haspopup="false" aria-role="button" class="" type="button"><i class="glyphicon play plexextplayer plexextplayerico plexextplayericocover"></i></button>');
  273. var newButton = template.appendTo(poster);
  274. newButton.click(clickListener);
  275. poster.mouseenter(function(){
  276. newButton.find('i').css('display','block');
  277. });
  278. poster.mouseleave(function(){
  279. newButton.find('i').css('display','none');
  280. });
  281. }
  282. }
  283. });
  284. };
  285.  
  286. // Make buttons smaller
  287. 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>');
  288.  
  289. // Bind buttons and check for new ones every 100ms
  290. setInterval(bindClicks, 100);
  291. bindClicks();