embyLaunchPotplayer

emby launch extetnal player

  1. // ==UserScript==
  2. // @name embyLaunchPotplayer
  3. // @name:en embyLaunchPotplayer
  4. // @name:zh embyLaunchPotplayer
  5. // @name:zh-CN embyLaunchPotplayer
  6. // @namespace http://tampermonkey.net/
  7. // @version 1.1.0
  8. // @description emby launch extetnal player
  9. // @description:zh-cn emby调用外部播放器
  10. // @description:en emby to external player
  11. // @license MIT
  12. // @author @bpking
  13. // @github https://github.com/bpking1/embyExternalUrl
  14. // @include */web/index.html
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict';
  19. function init() {
  20. let playBtns = document.getElementById("ExternalPlayersBtns");
  21. if (playBtns) {
  22. playBtns.remove();
  23. }
  24. let mainDetailButtons = document.querySelector("div[is='emby-scroller']:not(.hide) .mainDetailButtons");
  25. let buttonhtml = `<div id="ExternalPlayersBtns" class ="detailButtons flex align-items-flex-start flex-wrap-wrap">
  26. <button id="embyPot" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="Potplayer"> <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left icon-PotPlayer"> </i> <span class="button-text">Pot</span> </div> </button>
  27. <button id="embyVlc" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="VLC"> <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left icon-VLC"> </i> <span class="button-text">VLC</span> </div> </button>
  28. <button id="embyIINA" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="IINA"> <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left icon-IINA"> </i> <span class="button-text">IINA</span> </div> </button>
  29. <button id="embyNPlayer" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="NPlayer"> <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left icon-NPlayer"> </i> <span class="button-text">NPlayer</span> </div> </button>
  30. <button id="embyMX" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="MXPlayer"> <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left icon-MXPlayer"> </i> <span class="button-text">MX</span> </div> </button>
  31. <button id="embyInfuse" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="InfusePlayer"> <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left icon-infuse"> </i> <span class="button-text">Infuse</span> </div> </button>
  32. <button id="embyStellarPlayer" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="恒星播放器"> <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left icon-StellarPlayer"> </i> <span class="button-text">恒星</span> </div> </button>
  33. <button id="embyMPV" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="MPV"> <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left icon-MPV"> </i> <span class="button-text">MPV</span> </div> </button>
  34. <button id="embyCopyUrl" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="复制串流地址"> <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left icon-Copy"> </i> <span class="button-text">复制链接</span> </div> </button>
  35. </div>`
  36. mainDetailButtons.insertAdjacentHTML('afterend', buttonhtml)
  37. document.querySelector("div[is='emby-scroller']:not(.hide) #embyPot").onclick = embyPot;
  38. document.querySelector("div[is='emby-scroller']:not(.hide) #embyIINA").onclick = embyIINA;
  39. document.querySelector("div[is='emby-scroller']:not(.hide) #embyNPlayer").onclick = embyNPlayer;
  40. document.querySelector("div[is='emby-scroller']:not(.hide) #embyMX").onclick = embyMX;
  41. document.querySelector("div[is='emby-scroller']:not(.hide) #embyCopyUrl").onclick = embyCopyUrl;
  42. document.querySelector("div[is='emby-scroller']:not(.hide) #embyVlc").onclick = embyVlc;
  43. document.querySelector("div[is='emby-scroller']:not(.hide) #embyInfuse").onclick = embyInfuse;
  44. document.querySelector("div[is='emby-scroller']:not(.hide) #embyStellarPlayer").onclick = embyStellarPlayer;
  45. document.querySelector("div[is='emby-scroller']:not(.hide) #embyMPV").onclick = embyMPV;
  46.  
  47. //add icons
  48. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-PotPlayer").style.cssText += 'background: url(https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.5/embyWebAddExternalUrl/icons/icon-PotPlayer.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  49. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-IINA").style.cssText += 'background: url(https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.5/embyWebAddExternalUrl/icons/icon-IINA.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  50. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-MXPlayer").style.cssText += 'background: url(https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.5/embyWebAddExternalUrl/icons/icon-MXPlayer.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  51. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-infuse").style.cssText += 'background: url(https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.5/embyWebAddExternalUrl/icons/icon-infuse.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  52. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-VLC").style.cssText += 'background: url(https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.5/embyWebAddExternalUrl/icons/icon-VLC.webp)no-repeat;background-size: 100% 100%;font-size: 1.3em';
  53. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-NPlayer").style.cssText += 'background: url(https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.5/embyWebAddExternalUrl/icons/icon-NPlayer.webp)no-repeat;background-size: 100% 100%;font-size: 1.3em';
  54. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-StellarPlayer").style.cssText += 'background: url(https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.5/embyWebAddExternalUrl/icons/icon-StellarPlayer.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  55. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-MPV").style.cssText += 'background: url(https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.5/embyWebAddExternalUrl/icons/icon-MPV.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  56. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-Copy").style.cssText += 'background: url(https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.5/embyWebAddExternalUrl/icons/icon-Copy.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  57. }
  58.  
  59. function showFlag() {
  60. let mainDetailButtons = document.querySelector("div[is='emby-scroller']:not(.hide) .mainDetailButtons");
  61. if (!mainDetailButtons) {
  62. return false;
  63. }
  64. let videoElement = document.querySelector("div[is='emby-scroller']:not(.hide) .selectVideoContainer");
  65. if (videoElement && videoElement.classList.contains("hide")) {
  66. return false;
  67. }
  68. let audioElement = document.querySelector("div[is='emby-scroller']:not(.hide) .selectAudioContainer");
  69. return !(audioElement && audioElement.classList.contains("hide"));
  70. }
  71.  
  72. async function getItemInfo() {
  73. let userId = ApiClient._serverInfo.UserId;
  74. let itemId = /\?id=(\d*)/.exec(window.location.hash)[1];
  75. let response = await ApiClient.getItem(userId, itemId);
  76. //继续播放当前剧集的下一集
  77. if (response.Type == "Series") {
  78. let seriesNextUpItems = await ApiClient.getNextUpEpisodes({ SeriesId: itemId, UserId: userId });
  79. console.log("nextUpItemId: " + seriesNextUpItems.Items[0].Id);
  80. return await ApiClient.getItem(userId, seriesNextUpItems.Items[0].Id);
  81. }
  82. //播放当前季season的第一集
  83. if (response.Type == "Season") {
  84. let seasonItems = await ApiClient.getItems(userId, { parentId: itemId });
  85. console.log("seasonItemId: " + seasonItems.Items[0].Id);
  86. return await ApiClient.getItem(userId, seasonItems.Items[0].Id);
  87. }
  88. //播放当前集或电影
  89. console.log("itemId: " + itemId);
  90. return response;
  91. }
  92.  
  93. function getSeek(position) {
  94. let ticks = position * 10000;
  95. let parts = []
  96. , hours = ticks / 36e9;
  97. (hours = Math.floor(hours)) && parts.push(hours);
  98. let minutes = (ticks -= 36e9 * hours) / 6e8;
  99. ticks -= 6e8 * (minutes = Math.floor(minutes)),
  100. minutes < 10 && hours && (minutes = "0" + minutes),
  101. parts.push(minutes);
  102. let seconds = ticks / 1e7;
  103. return (seconds = Math.floor(seconds)) < 10 && (seconds = "0" + seconds),
  104. parts.push(seconds),
  105. parts.join(":")
  106. }
  107.  
  108. function getSubPath(mediaSource) {
  109. let selectSubtitles = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectSubtitles");
  110. let subTitlePath = '';
  111. //返回选中的外挂字幕
  112. if (selectSubtitles && selectSubtitles.value > 0) {
  113. let SubIndex = mediaSource.MediaStreams.findIndex(m => m.Index == selectSubtitles.value && m.IsExternal);
  114. if (SubIndex > -1) {
  115. let subtitleCodec = mediaSource.MediaStreams[SubIndex].Codec;
  116. subTitlePath = `/${mediaSource.Id}/Subtitles/${selectSubtitles.value}/Stream.${subtitleCodec}`;
  117. }
  118. }
  119. else {
  120. //默认尝试返回第一个外挂中文字幕
  121. let chiSubIndex = mediaSource.MediaStreams.findIndex(m => m.Language == "chi" && m.IsExternal);
  122. if (chiSubIndex > -1) {
  123. let subtitleCodec = mediaSource.MediaStreams[chiSubIndex].Codec;
  124. subTitlePath = `/${mediaSource.Id}/Subtitles/${chiSubIndex}/Stream.${subtitleCodec}`;
  125. } else {
  126. //尝试返回第一个外挂字幕
  127. let externalSubIndex = mediaSource.MediaStreams.findIndex(m => m.IsExternal);
  128. if (externalSubIndex > -1) {
  129. let subtitleCodec = mediaSource.MediaStreams[externalSubIndex].Codec;
  130. subTitlePath = `/${mediaSource.Id}/Subtitles/${externalSubIndex}/Stream.${subtitleCodec}`;
  131. }
  132. }
  133.  
  134. }
  135. return subTitlePath;
  136. }
  137.  
  138.  
  139. async function getEmbyMediaInfo() {
  140. let itemInfo = await getItemInfo();
  141. let mediaSourceId = itemInfo.MediaSources[0].Id;
  142. let selectSource = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectSource");
  143. if (selectSource && selectSource.value.length > 0) {
  144. mediaSourceId = selectSource.value;
  145. }
  146. //let selectAudio = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectAudio");
  147. let mediaSource = itemInfo.MediaSources.find(m => m.Id == mediaSourceId);
  148. let domain = `${ApiClient._serverAddress}/emby/videos/${itemInfo.Id}`;
  149. let subPath = getSubPath(mediaSource);
  150. let subUrl = subPath.length > 0 ? `${domain}${subPath}?api_key=${ApiClient.accessToken()}` : '';
  151. let streamUrl = `${domain}/stream.${mediaSource.Container}?api_key=${ApiClient.accessToken()}&Static=true&MediaSourceId=${mediaSourceId}`;
  152. let position = parseInt(itemInfo.UserData.PlaybackPositionTicks / 10000);
  153. let intent = await getIntent(mediaSource, position);
  154. console.log(streamUrl, subUrl, intent);
  155. return {
  156. streamUrl: streamUrl,
  157. subUrl: subUrl,
  158. intent: intent,
  159. }
  160. }
  161.  
  162. async function getIntent(mediaSource, position) {
  163. let title = mediaSource.Path.split('/').pop();
  164. let externalSubs = mediaSource.MediaStreams.filter(m => m.IsExternal == true);
  165. let subs = ''; //要求是android.net.uri[] ?
  166. let subs_name = '';
  167. let subs_filename = '';
  168. let subs_enable = '';
  169. if (externalSubs) {
  170. subs_name = externalSubs.map(s => s.DisplayTitle);
  171. subs_filename = externalSubs.map(s => s.Path.split('/').pop());
  172. }
  173. return {
  174. title: title,
  175. position: position,
  176. subs: subs,
  177. subs_name: subs_name,
  178. subs_filename: subs_filename,
  179. subs_enable: subs_enable
  180. };
  181. }
  182.  
  183. async function embyPot() {
  184. let mediaInfo = await getEmbyMediaInfo();
  185. let intent = mediaInfo.intent;
  186. let poturl = `potplayer://${encodeURI(mediaInfo.streamUrl)} /sub=${encodeURI(mediaInfo.subUrl)} /current /title="${intent.title}" /seek=${getSeek(intent.position)}`;
  187. console.log(poturl);
  188. window.open(poturl, "_blank");
  189. }
  190.  
  191. //https://wiki.videolan.org/Android_Player_Intents/
  192. async function embyVlc() {
  193. let mediaInfo = await getEmbyMediaInfo();
  194. let intent = mediaInfo.intent;
  195. //android subtitles: https://code.videolan.org/videolan/vlc-android/-/issues/1903
  196. let vlcUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=org.videolan.vlc;type=video/*;S.subtitles_location=${encodeURI(mediaInfo.subUrl)};S.title=${encodeURI(intent.title)};i.position=${intent.position};end`;
  197. if (getOS() == "windows") {
  198. //桌面端需要额外设置,参考这个项目: https://github.com/stefansundin/vlc-protocol
  199. vlcUrl = `vlc://${encodeURI(mediaInfo.streamUrl)}`;
  200. }
  201. if (getOS() == 'ios') {
  202. //https://code.videolan.org/videolan/vlc-ios/-/commit/55e27ed69e2fce7d87c47c9342f8889fda356aa9
  203. vlcUrl = `vlc-x-callback://x-callback-url/stream?url=${encodeURIComponent(mediaInfo.streamUrl)}&sub=${encodeURIComponent(mediaInfo.subUrl)}`;
  204. }
  205. console.log(vlcUrl);
  206. window.open(vlcUrl, "_blank");
  207. }
  208.  
  209. //https://github.com/iina/iina/issues/1991
  210. async function embyIINA() {
  211. let mediaInfo = await getEmbyMediaInfo();
  212. let iinaUrl = `iina://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}&new_window=1`;
  213. console.log(`iinaUrl= ${iinaUrl}`);
  214. window.open(iinaUrl, "_blank");
  215. }
  216.  
  217. //https://sites.google.com/site/mxvpen/api
  218. async function embyMX() {
  219. let mediaInfo = await getEmbyMediaInfo();
  220. let intent = mediaInfo.intent;
  221. //mxPlayer free
  222. let mxUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=com.mxtech.videoplayer.ad;S.title=${encodeURI(intent.title)};i.position=${intent.position};end`;
  223. //mxPlayer Pro
  224. //let mxUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=com.mxtech.videoplayer.pro;S.title=${encodeURI(intent.title)};i.position=${intent.position};end`;
  225. console.log(mxUrl);
  226. window.open(mxUrl, "_blank");
  227. }
  228.  
  229. async function embyNPlayer() {
  230. let mediaInfo = await getEmbyMediaInfo();
  231. let nUrl = getOS() == 'macOS' ? `nplayer-mac://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}&new_window=1` : `nplayer-${encodeURI(mediaInfo.streamUrl)}`;
  232. console.log(nUrl);
  233. window.open(nUrl, "_blank");
  234. }
  235.  
  236. //infuse
  237. async function embyInfuse() {
  238. let mediaInfo = await getEmbyMediaInfo();
  239. let infuseUrl = `infuse://x-callback-url/play?url=${encodeURIComponent(mediaInfo.streamUrl)}`;
  240. console.log(`infuseUrl= ${infuseUrl}`);
  241. window.open(infuseUrl, "_blank");
  242. }
  243.  
  244. //StellarPlayer
  245. async function embyStellarPlayer() {
  246. let mediaInfo = await getEmbyMediaInfo();
  247. let stellarPlayerUrl = `stellar://play/${encodeURI(mediaInfo.streamUrl)}`;
  248. console.log(`stellarPlayerUrl= ${stellarPlayerUrl}`);
  249. window.open(stellarPlayerUrl, "_blank");
  250. }
  251.  
  252. //MPV
  253. async function embyMPV() {
  254. let mediaInfo = await getEmbyMediaInfo();
  255. //桌面端需要额外设置,使用这个项目: https://github.com/akiirui/mpv-handler
  256. let streamUrl64 = btoa(mediaInfo.streamUrl).replace(/\//g, "_").replace(/\+/g, "-").replace(/\=/g, "");
  257. let MPVUrl = `mpv://play/${streamUrl64}`;
  258. if (mediaInfo.subUrl.length > 0) {
  259. let subUrl64 = btoa(mediaInfo.subUrl).replace(/\//g, "_").replace(/\+/g, "-").replace(/\=/g, "");
  260. MPVUrl = `mpv://play/${streamUrl64}/?subfile=${subUrl64}`;
  261. }
  262.  
  263. if (getOS() == "ios" || getOS() == "android") {
  264. MPVUrl = `mpv://${encodeURI(mediaInfo.streamUrl)}`;
  265. }
  266.  
  267. console.log(MPVUrl);
  268. window.open(MPVUrl, "_blank");
  269. }
  270.  
  271. async function embyCopyUrl() {
  272. let mediaInfo = await getEmbyMediaInfo();
  273. let textarea = document.createElement('textarea');
  274. document.body.appendChild(textarea);
  275. textarea.style.position = 'absolute';
  276. textarea.style.clip = 'rect(0 0 0 0)';
  277. textarea.value = mediaInfo.streamUrl;
  278. textarea.select();
  279. if (document.execCommand('copy', true)) {
  280. console.log(`copyUrl = ${mediaInfo.streamUrl}`);
  281. this.innerText = '复制成功';
  282. }
  283. //need https
  284. // if (navigator.clipboard) {
  285. // navigator.clipboard.writeText(mediaInfo.streamUrl).then(() => {
  286. // console.log(`copyUrl = ${mediaInfo.streamUrl}`);
  287. // this.innerText = '复制成功';
  288. // })
  289. // }
  290. }
  291.  
  292. function getOS() {
  293. let u = navigator.userAgent
  294. if (!!u.match(/compatible/i) || u.match(/Windows/i)) {
  295. return 'windows'
  296. } else if (!!u.match(/Macintosh/i) || u.match(/MacIntel/i)) {
  297. return 'macOS'
  298. } else if (!!u.match(/iphone/i) || u.match(/Ipad/i)) {
  299. return 'ios'
  300. } else if (u.match(/android/i)) {
  301. return 'android'
  302. } else if (u.match(/Ubuntu/i)) {
  303. return 'Ubuntu'
  304. } else {
  305. return 'other'
  306. }
  307. }
  308.  
  309. // monitor dom changements
  310. document.addEventListener("viewbeforeshow", function (e) {
  311. if (e.detail.contextPath.startsWith("/item?id=") ) {
  312. const mutation = new MutationObserver(function() {
  313. if (showFlag()) {
  314. init();
  315. mutation.disconnect();
  316. }
  317. })
  318. mutation.observe(document.body, {
  319. childList: true,
  320. characterData: true,
  321. subtree: true,
  322. })
  323. }
  324. });
  325.  
  326. })();