skipIntroPlugin.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. let tvIntro;
  2. class skipIntroPlugin {
  3. constructor({ events, playbackManager, ServerConnections }) {
  4. this.name = 'Skip Intro Plugin';
  5. this.type = 'input';
  6. this.id = 'skipIntroPlugin';
  7. (async() => {
  8. const api = await window.apiPromise;
  9. const enabled = await new Promise(resolve => {
  10. api.settings.value('plugins', 'skipintro', resolve);
  11. });
  12. console.log("Skip Intro Plugin enabled: " + enabled);
  13. if (!enabled) return;
  14. // Based on https://github.com/jellyfin/jellyfin-web/compare/release-10.8.z...ConfusedPolarBear:jellyfin-web:intros
  15. // Adapted for use in JMP
  16. const stylesheet = `
  17. <style>
  18. @media (hover:hover) and (pointer:fine) {
  19. .skipIntro .paper-icon-button-light:hover:not(:disabled) {
  20. color:black !important;
  21. background-color:rgba(47,93,98,0) !important;
  22. }
  23. }
  24. .skipIntro {
  25. padding: 0 1px;
  26. position: absolute;
  27. right: 10em;
  28. bottom: 9em;
  29. background-color:rgba(25, 25, 25, 0.66);
  30. border: 1px solid;
  31. border-radius: 0px;
  32. display: inline-block;
  33. cursor: pointer;
  34. box-shadow: inset 0 0 0 0 #f9f9f9;
  35. -webkit-transition: ease-out 0.4s;
  36. -moz-transition: ease-out 0.4s;
  37. transition: ease-out 0.4s;
  38. }
  39. @media (max-width: 1080px) {
  40. .skipIntro {
  41. right: 10%;
  42. }
  43. }
  44. .skipIntro:hover {
  45. box-shadow: inset 400px 0 0 0 #f9f9f9;
  46. -webkit-transition: ease-in 1s;
  47. -moz-transition: ease-in 1s;
  48. transition: ease-in 1s;
  49. }
  50. </style>
  51. `;
  52. document.head.insertAdjacentHTML('beforeend', stylesheet);
  53. const skipIntroHtml = `
  54. <div class="skipIntro hide">
  55. <button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light">
  56. Skip Intro
  57. <span class="material-icons skip_next"></span>
  58. </button>
  59. </div>
  60. `;
  61. function waitForElement(element, maxWait = 10000) {
  62. return new Promise((resolve, reject) => {
  63. const interval = setInterval(() => {
  64. const result = document.querySelector(element);
  65. if (result) {
  66. clearInterval(interval);
  67. resolve(result);
  68. }
  69. }, 100);
  70. setTimeout(() => {
  71. clearInterval(interval);
  72. reject();
  73. }, maxWait);
  74. });
  75. }
  76. async function injectSkipIntroHtml() {
  77. const playerContainer = await waitForElement('.upNextContainer', 5000);
  78. // inject only if it doesn't exist
  79. if (!document.querySelector('.skipIntro .btnSkipIntro')) {
  80. playerContainer.insertAdjacentHTML('afterend', skipIntroHtml);
  81. }
  82. document.querySelector('.skipIntro .btnSkipIntro').addEventListener('click', (e) => {
  83. e.preventDefault();
  84. e.stopPropagation();
  85. skipIntro();
  86. }, { useCapture: true });
  87. if (window.PointerEvent) {
  88. document.querySelector('.skipIntro .btnSkipIntro').addEventListener('pointerdown', (e) => {
  89. e.preventDefault();
  90. e.stopPropagation();
  91. }, { useCapture: true });
  92. }
  93. }
  94. function onPlayback(e, player, state) {
  95. if (state.NowPlayingItem) {
  96. getIntroTimestamps(state.NowPlayingItem);
  97. const onTimeUpdate = async () => {
  98. // Check if an introduction sequence was detected for this item.
  99. if (!tvIntro?.Valid) {
  100. return;
  101. }
  102. const seconds = playbackManager.currentTime(player) / 1000;
  103. await injectSkipIntroHtml(); // I have trust issues
  104. const skipIntro = document.querySelector(".skipIntro");
  105. // If the skip prompt should be shown, show it.
  106. if (seconds >= tvIntro.ShowSkipPromptAt && seconds < tvIntro.HideSkipPromptAt) {
  107. skipIntro.classList.remove("hide");
  108. return;
  109. }
  110. skipIntro.classList.add("hide");
  111. };
  112. events.on(player, 'timeupdate', onTimeUpdate);
  113. const onPlaybackStop = () => {
  114. events.off(player, 'timeupdate', onTimeUpdate);
  115. events.off(player, 'playbackstop', onPlaybackStop);
  116. };
  117. events.on(player, 'playbackstop', onPlaybackStop);
  118. }
  119. };
  120. events.on(playbackManager, 'playbackstart', onPlayback);
  121. function getIntroTimestamps(item) {
  122. const apiClient = ServerConnections
  123. ? ServerConnections.currentApiClient()
  124. : window.ApiClient;
  125. const address = apiClient.serverAddress();
  126. const url = `${address}/Episode/${item.Id}/IntroTimestamps`;
  127. const reqInit = {
  128. headers: {
  129. "Authorization": `MediaBrowser Token=${apiClient.accessToken()}`
  130. }
  131. };
  132. fetch(url, reqInit).then(r => {
  133. if (!r.ok) {
  134. tvIntro = null;
  135. return;
  136. }
  137. return r.json();
  138. }).then(intro => {
  139. tvIntro = intro;
  140. }).catch(err => { tvIntro = null; });
  141. }
  142. function skipIntro() {
  143. playbackManager.seekMs(tvIntro.IntroEnd * 1000);
  144. }
  145. })();
  146. }
  147. }
  148. window._skipIntroPlugin = skipIntroPlugin;