embyLaunchPotplayer

emby launch extetnal player

目前为 2023-03-08 提交的版本。查看 最新版本

  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.0.7
  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. setInterval(function () {
  20. let potplayer = document.querySelectorAll("div[is='emby-scroller']:not(.hide) #embyPot")[0];
  21. if (!potplayer) {
  22. let mainDetailButtons = document.querySelectorAll("div[is='emby-scroller']:not(.hide) .mainDetailButtons")[0];
  23. if (mainDetailButtons) {
  24. let buttonhtml = `<div class ="detailButtons flex align-items-flex-start flex-wrap-wrap">
  25. <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>
  26. <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>
  27. <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>
  28. <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>
  29. <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>
  30. <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>
  31. <!--<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>-->
  32. <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>
  33. </div>`
  34. mainDetailButtons.insertAdjacentHTML('afterend', buttonhtml)
  35. document.querySelector("div[is='emby-scroller']:not(.hide) #embyPot").onclick = embyPot;
  36. document.querySelector("div[is='emby-scroller']:not(.hide) #embyIINA").onclick = embyIINA;
  37. document.querySelector("div[is='emby-scroller']:not(.hide) #embyNPlayer").onclick = embyNPlayer;
  38. document.querySelector("div[is='emby-scroller']:not(.hide) #embyMX").onclick = embyMX;
  39. document.querySelector("div[is='emby-scroller']:not(.hide) #embyCopyUrl").onclick = embyCopyUrl;
  40. document.querySelector("div[is='emby-scroller']:not(.hide) #embyVlc").onclick = embyVlc;
  41. document.querySelector("div[is='emby-scroller']:not(.hide) #embyInfuse").onclick = embyInfuse;
  42. //恒星播放器,需要的可以自己打开
  43. //document.querySelector("div[is='emby-scroller']:not(.hide) #embyStellarPlayer").onclick = embyStellarPlayer;
  44.  
  45. //add icons
  46. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-PotPlayer").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.3/embyWebAddExternalUrl/icons/icon-PotPlayer.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  47. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-IINA").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.3/embyWebAddExternalUrl/icons/icon-IINA.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  48. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-MXPlayer").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.3/embyWebAddExternalUrl/icons/icon-MXPlayer.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  49. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-infuse").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.3/embyWebAddExternalUrl/icons/icon-infuse.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  50. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-VLC").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.3/embyWebAddExternalUrl/icons/icon-VLC.webp)no-repeat;background-size: 100% 100%;font-size: 1.3em';
  51. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-NPlayer").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.3/embyWebAddExternalUrl/icons/icon-NPlayer.webp)no-repeat;background-size: 100% 100%;font-size: 1.3em';
  52. //document.querySelector("div[is='emby-scroller']:not(.hide) .icon-StellarPlayer").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.3/embyWebAddExternalUrl/icons/icon-StellarPlayer.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  53. document.querySelector("div[is='emby-scroller']:not(.hide) .icon-Copy").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/embyExternalUrl@0.0.3/embyWebAddExternalUrl/icons/icon-Copy.webp)no-repeat;background-size: 100% 100%;font-size: 1.4em';
  54. }
  55. }
  56. }, 1000);
  57.  
  58.  
  59. async function getItemInfo() {
  60. let userId = ApiClient._serverInfo.UserId;
  61. let itemId = /\?id=(\d*)/.exec(window.location.hash)[1];
  62. let response = await ApiClient.getItem(userId, itemId);
  63. //继续播放当前剧集的下一集
  64. if (response.Type == "Series") {
  65. let seriesNextUpItems = await ApiClient.getNextUpEpisodes({ SeriesId: itemId, UserId: userId });
  66. console.log("nextUpItemId: " + seriesNextUpItems.Items[0].Id);
  67. return await ApiClient.getItem(userId, seriesNextUpItems.Items[0].Id);
  68. }
  69. //播放当前季season的第一集
  70. if (response.Type == "Season") {
  71. let seasonItems = await ApiClient.getItems(userId, { parentId: itemId });
  72. console.log("seasonItemId: " + seasonItems.Items[0].Id);
  73. return await ApiClient.getItem(userId, seasonItems.Items[0].Id);
  74. }
  75. //播放当前集或电影
  76. console.log("itemId: " + itemId);
  77. return response;
  78. }
  79.  
  80. function getSeek(position) {
  81. let ticks = position * 10000;
  82. let parts = []
  83. , hours = ticks / 36e9;
  84. (hours = Math.floor(hours)) && parts.push(hours);
  85. let minutes = (ticks -= 36e9 * hours) / 6e8;
  86. ticks -= 6e8 * (minutes = Math.floor(minutes)),
  87. minutes < 10 && hours && (minutes = "0" + minutes),
  88. parts.push(minutes);
  89. let seconds = ticks / 1e7;
  90. return (seconds = Math.floor(seconds)) < 10 && (seconds = "0" + seconds),
  91. parts.push(seconds),
  92. parts.join(":")
  93. }
  94.  
  95. function getSubPath(mediaSource) {
  96. let selectSubtitles = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectSubtitles");
  97. let subTitlePath = '';
  98. //返回选中的外挂字幕
  99. if (selectSubtitles && selectSubtitles.value > 0) {
  100. let SubIndex = mediaSource.MediaStreams.findIndex(m => m.Index == selectSubtitles.value && m.IsExternal);
  101. if (SubIndex > -1) {
  102. let subtitleCodec = mediaSource.MediaStreams[SubIndex].Codec;
  103. subTitlePath = `/${mediaSource.Id}/Subtitles/${selectSubtitles.value}/Stream.${subtitleCodec}`;
  104. }
  105. }
  106. else {
  107. //默认尝试返回第一个外挂中文字幕
  108. let chiSubIndex = mediaSource.MediaStreams.findIndex(m => m.Language == "chi" && m.IsExternal);
  109. if (chiSubIndex > -1) {
  110. let subtitleCodec = mediaSource.MediaStreams[chiSubIndex].Codec;
  111. subTitlePath = `/${mediaSource.Id}/Subtitles/${chiSubIndex}/Stream.${subtitleCodec}`;
  112. } else {
  113. //尝试返回第一个外挂字幕
  114. let externalSubIndex = mediaSource.MediaStreams.findIndex(m => m.IsExternal);
  115. if (externalSubIndex > -1) {
  116. let subtitleCodec = mediaSource.MediaStreams[externalSubIndex].Codec;
  117. subTitlePath = `/${mediaSource.Id}/Subtitles/${externalSubIndex}/Stream.${subtitleCodec}`;
  118. }
  119. }
  120.  
  121. }
  122. return subTitlePath;
  123. }
  124.  
  125.  
  126. async function getEmbyMediaInfo() {
  127. let itemInfo = await getItemInfo();
  128. let mediaSourceId = itemInfo.MediaSources[0].Id;
  129. let selectSource = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectSource");
  130. if (selectSource && selectSource.value.length > 0) {
  131. mediaSourceId = selectSource.value;
  132. }
  133. //let selectAudio = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectAudio");
  134. let mediaSource = itemInfo.MediaSources.find(m => m.Id == mediaSourceId);
  135. let domain = `${ApiClient._serverAddress}/emby/videos/${itemInfo.Id}`;
  136. let subPath = getSubPath(mediaSource);
  137. let subUrl = subPath.length > 0 ? `${domain}${subPath}?api_key=${ApiClient.accessToken()}` : '';
  138. let streamUrl = `${domain}/stream.${mediaSource.Container}?api_key=${ApiClient.accessToken()}&Static=true&MediaSourceId=${mediaSourceId}`;
  139. let position = parseInt(itemInfo.UserData.PlaybackPositionTicks / 10000);
  140. let intent = await getIntent(mediaSource, position);
  141. console.log(streamUrl, subUrl, intent);
  142. return {
  143. streamUrl: streamUrl,
  144. subUrl: subUrl,
  145. intent: intent,
  146. }
  147. }
  148.  
  149. async function getIntent(mediaSource, position) {
  150. let title = mediaSource.Path.split('/').pop();
  151. let externalSubs = mediaSource.MediaStreams.filter(m => m.IsExternal == true);
  152. let subs = ''; //要求是android.net.uri[] ?
  153. let subs_name = '';
  154. let subs_filename = '';
  155. let subs_enable = '';
  156. if (externalSubs) {
  157. subs_name = externalSubs.map(s => s.DisplayTitle);
  158. subs_filename = externalSubs.map(s => s.Path.split('/').pop());
  159. }
  160. return {
  161. title: title,
  162. position: position,
  163. subs: subs,
  164. subs_name: subs_name,
  165. subs_filename: subs_filename,
  166. subs_enable: subs_enable
  167. };
  168. }
  169.  
  170. async function embyPot() {
  171. let mediaInfo = await getEmbyMediaInfo();
  172. let intent = mediaInfo.intent;
  173. let poturl = `potplayer://${encodeURI(mediaInfo.streamUrl)} /sub=${encodeURI(mediaInfo.subUrl)} /current /title="${intent.title}" /seek=${getSeek(intent.position)}`;
  174. console.log(poturl);
  175. window.open(poturl, "_blank");
  176. }
  177.  
  178. //https://wiki.videolan.org/Android_Player_Intents/
  179. async function embyVlc() {
  180. let mediaInfo = await getEmbyMediaInfo();
  181. let intent = mediaInfo.intent;
  182. //android subtitles: https://code.videolan.org/videolan/vlc-android/-/issues/1903
  183. 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`;
  184. if (getOS() == "windows") {
  185. //桌面端需要额外设置,参考这个项目,MPV也是类似的方法: https://github.com/stefansundin/vlc-protocol
  186. vlcUrl = `vlc://${encodeURI(mediaInfo.streamUrl)}`;
  187. }
  188. if (getOS() == 'ios') {
  189. //https://code.videolan.org/videolan/vlc-ios/-/commit/55e27ed69e2fce7d87c47c9342f8889fda356aa9
  190. vlcUrl = `vlc-x-callback://x-callback-url/stream?url=${encodeURIComponent(mediaInfo.streamUrl)}&sub=${encodeURIComponent(mediaInfo.subUrl)}`;
  191. }
  192. console.log(vlcUrl);
  193. window.open(vlcUrl, "_blank");
  194. }
  195.  
  196. //https://github.com/iina/iina/issues/1991
  197. async function embyIINA() {
  198. let mediaInfo = await getEmbyMediaInfo();
  199. let iinaUrl = `iina://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}&new_window=1`;
  200. console.log(`iinaUrl= ${iinaUrl}`);
  201. window.open(iinaUrl, "_blank");
  202. }
  203.  
  204. //https://sites.google.com/site/mxvpen/api
  205. async function embyMX() {
  206. let mediaInfo = await getEmbyMediaInfo();
  207. let intent = mediaInfo.intent;
  208. //mxPlayer free
  209. let mxUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=com.mxtech.videoplayer.ad;S.title=${encodeURI(intent.title)};i.position=${intent.position};end`;
  210. //mxPlayer Pro
  211. //let mxUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=com.mxtech.videoplayer.pro;S.title=${encodeURI(intent.title)};i.position=${intent.position};end`;
  212. console.log(mxUrl);
  213. window.open(mxUrl, "_blank");
  214. }
  215.  
  216. async function embyNPlayer() {
  217. let mediaInfo = await getEmbyMediaInfo();
  218. let nUrl = getOS() == 'macOS' ? `nplayer-mac://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}&new_window=1` : `nplayer-${encodeURI(mediaInfo.streamUrl)}`;
  219. console.log(nUrl);
  220. window.open(nUrl, "_blank");
  221. }
  222.  
  223. //infuse
  224. async function embyInfuse() {
  225. let mediaInfo = await getEmbyMediaInfo();
  226. let infuseUrl = `infuse://x-callback-url/play?url=${encodeURIComponent(mediaInfo.streamUrl)}`;
  227. console.log(`infuseUrl= ${infuseUrl}`);
  228. window.open(infuseUrl, "_blank");
  229. }
  230.  
  231. //StellarPlayer
  232. async function embyStellarPlayer() {
  233. let mediaInfo = await getEmbyMediaInfo();
  234. let stellarPlayerUrl = `stellar://play/${encodeURI(mediaInfo.streamUrl)}`;
  235. console.log(`stellarPlayerUrl= ${stellarPlayerUrl}`);
  236. window.open(stellarPlayerUrl, "_blank");
  237. }
  238.  
  239. async function embyCopyUrl() {
  240. let mediaInfo = await getEmbyMediaInfo();
  241. let textarea = document.createElement('textarea');
  242. document.body.appendChild(textarea);
  243. textarea.style.position = 'absolute';
  244. textarea.style.clip = 'rect(0 0 0 0)';
  245. textarea.value = mediaInfo.streamUrl;
  246. textarea.select();
  247. if (document.execCommand('copy', true)) {
  248. console.log(`copyUrl = ${mediaInfo.streamUrl}`);
  249. this.innerText = '复制成功';
  250. }
  251. //need https
  252. // if (navigator.clipboard) {
  253. // navigator.clipboard.writeText(mediaInfo.streamUrl).then(() => {
  254. // console.log(`copyUrl = ${mediaInfo.streamUrl}`);
  255. // this.innerText = '复制成功';
  256. // })
  257. // }
  258. }
  259.  
  260. function getOS() {
  261. let u = navigator.userAgent
  262. if (!!u.match(/compatible/i) || u.match(/Windows/i)) {
  263. return 'windows'
  264. } else if (!!u.match(/Macintosh/i) || u.match(/MacIntel/i)) {
  265. return 'macOS'
  266. } else if (!!u.match(/iphone/i) || u.match(/Ipad/i)) {
  267. return 'ios'
  268. } else if (u.match(/android/i)) {
  269. return 'android'
  270. } else if (u.match(/Ubuntu/i)) {
  271. return 'Ubuntu'
  272. } else {
  273. return 'other'
  274. }
  275. }
  276.  
  277. })();
  278.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址